@@ -9,14 +9,17 @@ |
||
9 | 9 |
#= require ./worker-checker |
10 | 10 |
#= require_self |
11 | 11 |
|
12 |
-window.setupJsonEditor = ($editor = $(".live-json-editor")) -> |
|
12 |
+window.setupJsonEditor = ($editors = $(".live-json-editor")) -> |
|
13 | 13 |
JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
14 | 14 |
JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
15 |
- if $editor.length |
|
15 |
+ editors = [] |
|
16 |
+ $editors.each -> |
|
17 |
+ $editor = $(this) |
|
16 | 18 |
jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500) |
17 | 19 |
jsonEditor.doTruncation true |
18 | 20 |
jsonEditor.showFunctionButtons() |
19 |
- return jsonEditor |
|
21 |
+ editors.push jsonEditor |
|
22 |
+ return editors |
|
20 | 23 |
|
21 | 24 |
hideSchedule = -> |
22 | 25 |
$(".schedule-region select").hide() |
@@ -55,12 +58,15 @@ showEventDescriptions = -> |
||
55 | 58 |
|
56 | 59 |
$(document).ready -> |
57 | 60 |
# JSON Editor |
58 |
- window.jsonEditor = setupJsonEditor() |
|
61 |
+ window.jsonEditor = setupJsonEditor()[0] |
|
59 | 62 |
|
60 | 63 |
# Flash |
61 | 64 |
if $(".flash").length |
62 | 65 |
setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
63 | 66 |
|
67 |
+ # Help popovers |
|
68 |
+ $('.hover-help').popover(trigger: 'hover') |
|
69 |
+ |
|
64 | 70 |
# Agent Navigation |
65 | 71 |
$agentNavigate = $('#agent-navigate') |
66 | 72 |
|
@@ -99,7 +105,7 @@ $(document).ready -> |
||
99 | 105 |
e.preventDefault() |
100 | 106 |
$agentNavigate.focus() |
101 | 107 |
|
102 |
-# Agent Show |
|
108 |
+ # Agent Show |
|
103 | 109 |
fetchLogs = (e) -> |
104 | 110 |
agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
105 | 111 |
e.preventDefault() |
@@ -140,13 +140,29 @@ span.not-applicable:after { |
||
140 | 140 |
opacity: 0.5; |
141 | 141 |
} |
142 | 142 |
|
143 |
-// Fix JSON Editor |
|
143 |
+// JSON Editor |
|
144 |
+ |
|
145 |
+.live-json-editor { |
|
146 |
+ font-family: "Courier New", Courier, monospace; |
|
147 |
+} |
|
144 | 148 |
|
145 | 149 |
.json-editor blockquote { |
146 | 150 |
font-size: 14px; |
147 | 151 |
} |
148 | 152 |
|
149 |
-// Bootstrappy colour styles |
|
153 |
+// Position tweeks |
|
154 |
+ |
|
155 |
+.hover-help { |
|
156 |
+ top: 2px; |
|
157 |
+} |
|
158 |
+ |
|
159 |
+h2 .scenario, a span.label.scenario { |
|
160 |
+ position: relative; |
|
161 |
+ top: -2px; |
|
162 |
+} |
|
163 |
+ |
|
164 |
+// Bootstrappy color styles |
|
165 |
+ |
|
150 | 166 |
.color-danger { |
151 | 167 |
color: #d9534f; |
152 | 168 |
} |
@@ -0,0 +1,15 @@ |
||
1 |
+.scenario-import { |
|
2 |
+ .agent-import-list { |
|
3 |
+ .agent-import { |
|
4 |
+ margin-bottom: 20px; |
|
5 |
+ |
|
6 |
+ .instructions { |
|
7 |
+ margin-bottom: 10px; |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ .current { |
|
11 |
+ font-weight: bold; |
|
12 |
+ } |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+} |
@@ -29,7 +29,7 @@ module AssignableTypes |
||
29 | 29 |
const_get(:TYPES).include?(type) |
30 | 30 |
end |
31 | 31 |
|
32 |
- def build_for_type(type, user, attributes) |
|
32 |
+ def build_for_type(type, user, attributes = {}) |
|
33 | 33 |
attributes.delete(:type) |
34 | 34 |
|
35 | 35 |
if valid_type?(type) |
@@ -31,7 +31,4 @@ module EmailConcern |
||
31 | 31 |
def present_hash(hash, skip_key = nil) |
32 | 32 |
hash.to_a.sort_by {|a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless k.to_s == skip_key.to_s }.compact |
33 | 33 |
end |
34 |
- |
|
35 |
- module ClassMethods |
|
36 |
- end |
|
37 | 34 |
end |
@@ -0,0 +1,13 @@ |
||
1 |
+module HasGuid |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ before_save :make_guid |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ protected |
|
9 |
+ |
|
10 |
+ def make_guid |
|
11 |
+ self.guid = SecureRandom.hex unless guid.present? |
|
12 |
+ end |
|
13 |
+end |
@@ -1,22 +1,26 @@ |
||
1 | 1 |
module LiquidInterpolatable |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
- def interpolate_options(options, payload) |
|
5 |
- case options.class.to_s |
|
6 |
- when 'String' |
|
7 |
- interpolate_string(options, payload) |
|
8 |
- when 'ActiveSupport::HashWithIndifferentAccess', 'Hash' |
|
9 |
- duped_options = options.dup |
|
10 |
- duped_options.each do |key, value| |
|
11 |
- duped_options[key] = interpolate_options(value, payload) |
|
12 |
- end |
|
13 |
- when 'Array' |
|
14 |
- options.collect do |value| |
|
15 |
- interpolate_options(value, payload) |
|
16 |
- end |
|
4 |
+ def interpolate_options(options, payload = {}) |
|
5 |
+ case options |
|
6 |
+ when String |
|
7 |
+ interpolate_string(options, payload) |
|
8 |
+ when ActiveSupport::HashWithIndifferentAccess, Hash |
|
9 |
+ options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, payload); memo } |
|
10 |
+ when Array |
|
11 |
+ options.map { |value| interpolate_options(value, payload) } |
|
12 |
+ else |
|
13 |
+ options |
|
17 | 14 |
end |
18 | 15 |
end |
19 | 16 |
|
17 |
+ def interpolated(payload = {}) |
|
18 |
+ key = [options, payload] |
|
19 |
+ @interpolated_cache ||= {} |
|
20 |
+ @interpolated_cache[key] ||= interpolate_options(options, payload) |
|
21 |
+ @interpolated_cache[key] |
|
22 |
+ end |
|
23 |
+ |
|
20 | 24 |
def interpolate_string(string, payload) |
21 | 25 |
Liquid::Template.parse(string).render!(payload, registers: {agent: self}) |
22 | 26 |
end |
@@ -21,12 +21,12 @@ class AgentsController < ApplicationController |
||
21 | 21 |
end |
22 | 22 |
|
23 | 23 |
def run |
24 |
- agent = current_user.agents.find(params[:id]) |
|
25 |
- Agent.async_check(agent.id) |
|
26 |
- if params[:return] == "show" |
|
27 |
- redirect_to agent_path(agent), notice: "Agent run queued" |
|
28 |
- else |
|
29 |
- redirect_to agents_path, notice: "Agent run queued" |
|
24 |
+ @agent = current_user.agents.find(params[:id]) |
|
25 |
+ Agent.async_check(@agent.id) |
|
26 |
+ |
|
27 |
+ respond_to do |format| |
|
28 |
+ format.html { redirect_back "Agent run queued for '#{@agent.name}'" } |
|
29 |
+ format.json { head :ok } |
|
30 | 30 |
end |
31 | 31 |
end |
32 | 32 |
|
@@ -54,12 +54,20 @@ class AgentsController < ApplicationController |
||
54 | 54 |
def remove_events |
55 | 55 |
@agent = current_user.agents.find(params[:id]) |
56 | 56 |
@agent.events.delete_all |
57 |
- redirect_to agents_path, notice: "All events removed" |
|
57 |
+ |
|
58 |
+ respond_to do |format| |
|
59 |
+ format.html { redirect_back "All emitted events removed for '#{@agent.name}'" } |
|
60 |
+ format.json { head :ok } |
|
61 |
+ end |
|
58 | 62 |
end |
59 | 63 |
|
60 | 64 |
def propagate |
61 |
- details = Agent.receive! |
|
62 |
- redirect_to agents_path, notice: "Queued propagation calls for #{details[:event_count]} event(s) on #{details[:agent_count]} agent(s)" |
|
65 |
+ details = Agent.receive! # Eventually this should probably be scoped to the current_user. |
|
66 |
+ |
|
67 |
+ respond_to do |format| |
|
68 |
+ format.html { redirect_back "Queued propagation calls for #{details[:event_count]} event(s) on #{details[:agent_count]} agent(s)" } |
|
69 |
+ format.json { head :ok } |
|
70 |
+ end |
|
63 | 71 |
end |
64 | 72 |
|
65 | 73 |
def show |
@@ -91,7 +99,11 @@ class AgentsController < ApplicationController |
||
91 | 99 |
end |
92 | 100 |
|
93 | 101 |
def diagram |
94 |
- @agents = current_user.agents.includes(:receivers) |
|
102 |
+ @agents = if params[:scenario_id].present? |
|
103 |
+ current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) |
|
104 |
+ else |
|
105 |
+ current_user.agents.includes(:receivers) |
|
106 |
+ end |
|
95 | 107 |
end |
96 | 108 |
|
97 | 109 |
def create |
@@ -100,8 +112,8 @@ class AgentsController < ApplicationController |
||
100 | 112 |
params[:agent]) |
101 | 113 |
respond_to do |format| |
102 | 114 |
if @agent.save |
103 |
- format.html { redirect_to agents_path, notice: 'Your Agent was successfully created.' } |
|
104 |
- format.json { render json: @agent, status: :created, location: @agent } |
|
115 |
+ format.html { redirect_back "'#{@agent.name}' was successfully created." } |
|
116 |
+ format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
|
105 | 117 |
else |
106 | 118 |
format.html { render action: "new" } |
107 | 119 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
@@ -114,14 +126,8 @@ class AgentsController < ApplicationController |
||
114 | 126 |
|
115 | 127 |
respond_to do |format| |
116 | 128 |
if @agent.update_attributes(params[:agent]) |
117 |
- format.html { |
|
118 |
- if params[:return] == "show" |
|
119 |
- redirect_to agent_path(@agent), notice: 'Your Agent was successfully updated.' |
|
120 |
- else |
|
121 |
- redirect_to agents_path, notice: 'Your Agent was successfully updated.' |
|
122 |
- end |
|
123 |
- } |
|
124 |
- format.json { head :no_content } |
|
129 |
+ format.html { redirect_back "'#{@agent.name}' was successfully updated." } |
|
130 |
+ format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
|
125 | 131 |
else |
126 | 132 |
format.html { render action: "edit" } |
127 | 133 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
@@ -129,13 +135,39 @@ class AgentsController < ApplicationController |
||
129 | 135 |
end |
130 | 136 |
end |
131 | 137 |
|
138 |
+ def leave_scenario |
|
139 |
+ @agent = current_user.agents.find(params[:id]) |
|
140 |
+ @scenario = current_user.scenarios.find(params[:scenario_id]) |
|
141 |
+ @agent.scenarios.destroy(@scenario) |
|
142 |
+ |
|
143 |
+ respond_to do |format| |
|
144 |
+ format.html { redirect_back "'#{@agent.name}' removed from '#{@scenario.name}'" } |
|
145 |
+ format.json { head :no_content } |
|
146 |
+ end |
|
147 |
+ end |
|
148 |
+ |
|
132 | 149 |
def destroy |
133 | 150 |
@agent = current_user.agents.find(params[:id]) |
134 | 151 |
@agent.destroy |
135 | 152 |
|
136 | 153 |
respond_to do |format| |
137 |
- format.html { redirect_to agents_path } |
|
154 |
+ format.html { redirect_back "'#{@agent.name}' deleted" } |
|
138 | 155 |
format.json { head :no_content } |
139 | 156 |
end |
140 | 157 |
end |
158 |
+ |
|
159 |
+ protected |
|
160 |
+ |
|
161 |
+ # Sanitize params[:return] to prevent open redirect attacks, a common security issue. |
|
162 |
+ def redirect_back(message) |
|
163 |
+ if params[:return] == "show" && @agent |
|
164 |
+ path = agent_path(@agent) |
|
165 |
+ elsif params[:return] =~ /\A#{Regexp::escape scenarios_path}\/\d+\Z/ |
|
166 |
+ path = params[:return] |
|
167 |
+ else |
|
168 |
+ path = agents_path |
|
169 |
+ end |
|
170 |
+ |
|
171 |
+ redirect_to path, notice: message |
|
172 |
+ end |
|
141 | 173 |
end |
@@ -0,0 +1,20 @@ |
||
1 |
+class ScenarioImportsController < ApplicationController |
|
2 |
+ def new |
|
3 |
+ @scenario_import = ScenarioImport.new(:url => params[:url]) |
|
4 |
+ end |
|
5 |
+ |
|
6 |
+ def create |
|
7 |
+ @scenario_import = ScenarioImport.new(params[:scenario_import]) |
|
8 |
+ @scenario_import.set_user(current_user) |
|
9 |
+ |
|
10 |
+ if @scenario_import.will_request_local?(scenarios_url) |
|
11 |
+ render :text => 'Sorry, you cannot import a Scenario by URL from your own Huginn server.' and return |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ if @scenario_import.valid? && @scenario_import.should_import? && @scenario_import.import |
|
15 |
+ redirect_to @scenario_import.scenario, notice: "Import successful!" |
|
16 |
+ else |
|
17 |
+ render action: "new" |
|
18 |
+ end |
|
19 |
+ end |
|
20 |
+end |
@@ -0,0 +1,100 @@ |
||
1 |
+class ScenariosController < ApplicationController |
|
2 |
+ skip_before_filter :authenticate_user!, :only => :export |
|
3 |
+ |
|
4 |
+ def index |
|
5 |
+ @scenarios = current_user.scenarios.page(params[:page]) |
|
6 |
+ |
|
7 |
+ respond_to do |format| |
|
8 |
+ format.html |
|
9 |
+ format.json { render json: @scenarios } |
|
10 |
+ end |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def new |
|
14 |
+ @scenario = current_user.scenarios.build |
|
15 |
+ |
|
16 |
+ respond_to do |format| |
|
17 |
+ format.html |
|
18 |
+ format.json { render json: @scenario } |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ def show |
|
23 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
24 |
+ @agents = @scenario.agents.preload(:scenarios).page(params[:page]) |
|
25 |
+ |
|
26 |
+ respond_to do |format| |
|
27 |
+ format.html |
|
28 |
+ format.json { render json: @scenario } |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def share |
|
33 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
34 |
+ |
|
35 |
+ respond_to do |format| |
|
36 |
+ format.html |
|
37 |
+ format.json { render json: @scenario } |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def export |
|
42 |
+ @scenario = Scenario.find(params[:id]) |
|
43 |
+ raise ActiveRecord::RecordNotFound unless @scenario.public? || (current_user && current_user.id == @scenario.user_id) |
|
44 |
+ |
|
45 |
+ @exporter = AgentsExporter.new(:name => @scenario.name, |
|
46 |
+ :description => @scenario.description, |
|
47 |
+ :guid => @scenario.guid, |
|
48 |
+ :source_url => @scenario.public? && export_scenario_url(@scenario), |
|
49 |
+ :agents => @scenario.agents) |
|
50 |
+ response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"' |
|
51 |
+ render :json => JSON.pretty_generate(@exporter.as_json) |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ def edit |
|
55 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
56 |
+ |
|
57 |
+ respond_to do |format| |
|
58 |
+ format.html |
|
59 |
+ format.json { render json: @scenario } |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def create |
|
64 |
+ @scenario = current_user.scenarios.build(params[:scenario]) |
|
65 |
+ |
|
66 |
+ respond_to do |format| |
|
67 |
+ if @scenario.save |
|
68 |
+ format.html { redirect_to @scenario, notice: 'This Scenario was successfully created.' } |
|
69 |
+ format.json { render json: @scenario, status: :created, location: @scenario } |
|
70 |
+ else |
|
71 |
+ format.html { render action: "new" } |
|
72 |
+ format.json { render json: @scenario.errors, status: :unprocessable_entity } |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ def update |
|
78 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
79 |
+ |
|
80 |
+ respond_to do |format| |
|
81 |
+ if @scenario.update_attributes(params[:scenario]) |
|
82 |
+ format.html { redirect_to @scenario, notice: 'This Scenario was successfully updated.' } |
|
83 |
+ format.json { head :no_content } |
|
84 |
+ else |
|
85 |
+ format.html { render action: "edit" } |
|
86 |
+ format.json { render json: @scenario.errors, status: :unprocessable_entity } |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+ end |
|
90 |
+ |
|
91 |
+ def destroy |
|
92 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
93 |
+ @scenario.destroy |
|
94 |
+ |
|
95 |
+ respond_to do |format| |
|
96 |
+ format.html { redirect_to scenarios_path } |
|
97 |
+ format.json { head :no_content } |
|
98 |
+ end |
|
99 |
+ end |
|
100 |
+end |
@@ -6,6 +6,12 @@ module AgentHelper |
||
6 | 6 |
end |
7 | 7 |
end |
8 | 8 |
|
9 |
+ def scenario_links(agent) |
|
10 |
+ agent.scenarios.map { |scenario| |
|
11 |
+ link_to(scenario.name, scenario, class: "label label-info") |
|
12 |
+ }.join(" ").html_safe |
|
13 |
+ end |
|
14 |
+ |
|
9 | 15 |
def agent_show_class(agent) |
10 | 16 |
agent.short_type.underscore.dasherize |
11 | 17 |
end |
@@ -35,6 +35,7 @@ module DotHelper |
||
35 | 35 |
dot << '%s;' % disabled_label(agent) |
36 | 36 |
end |
37 | 37 |
agent.receivers.each do |receiver| |
38 |
+ next unless agents.include?(receiver) |
|
38 | 39 |
dot << "%s->%s;" % [disabled_label(agent), disabled_label(receiver)] |
39 | 40 |
end |
40 | 41 |
end |
@@ -12,6 +12,8 @@ class Agent < ActiveRecord::Base |
||
12 | 12 |
include JSONSerializedField |
13 | 13 |
include RDBMSFunctions |
14 | 14 |
include WorkingHelpers |
15 |
+ include LiquidInterpolatable |
|
16 |
+ include HasGuid |
|
15 | 17 |
|
16 | 18 |
markdown_class_attributes :description, :event_description |
17 | 19 |
|
@@ -22,13 +24,14 @@ class Agent < ActiveRecord::Base |
||
22 | 24 |
|
23 | 25 |
EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })] |
24 | 26 |
|
25 |
- attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :keep_events_for, :propagate_immediately |
|
27 |
+ attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :scenario_ids, :keep_events_for, :propagate_immediately |
|
26 | 28 |
|
27 | 29 |
json_serialize :options, :memory |
28 | 30 |
|
29 | 31 |
validates_presence_of :name, :user |
30 | 32 |
validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last) |
31 | 33 |
validate :sources_are_owned |
34 |
+ validate :scenarios_are_owned |
|
32 | 35 |
validate :validate_schedule |
33 | 36 |
validate :validate_options |
34 | 37 |
|
@@ -49,6 +52,8 @@ class Agent < ActiveRecord::Base |
||
49 | 52 |
has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver |
50 | 53 |
has_many :sources, :through => :links_as_receiver, :class_name => "Agent", :inverse_of => :receivers |
51 | 54 |
has_many :receivers, :through => :links_as_source, :class_name => "Agent", :inverse_of => :sources |
55 |
+ has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :agent |
|
56 |
+ has_many :scenarios, :through => :scenario_memberships, :inverse_of => :agents |
|
52 | 57 |
|
53 | 58 |
scope :of_type, lambda { |type| |
54 | 59 |
type = case type |
@@ -207,6 +212,10 @@ class Agent < ActiveRecord::Base |
||
207 | 212 |
errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
208 | 213 |
end |
209 | 214 |
|
215 |
+ def scenarios_are_owned |
|
216 |
+ errors.add(:scenarios, "must be owned by you") unless scenarios.all? {|s| s.user == user } |
|
217 |
+ end |
|
218 |
+ |
|
210 | 219 |
def validate_schedule |
211 | 220 |
unless cannot_be_scheduled? |
212 | 221 |
errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
@@ -54,9 +54,9 @@ module Agents |
||
54 | 54 |
end |
55 | 55 |
|
56 | 56 |
def check |
57 |
- auth_options = {:basic_auth => {:username =>options[:username], :password=>options['password']}} |
|
58 |
- parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(options['from'])}+to+#{URI.encode(options['to'])}", auth_options |
|
59 |
- fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(options['end_date'])}\\3#{date_to_unix_epoch(options['start_date'])}" |
|
57 |
+ auth_options = {:basic_auth => {:username =>interpolated[:username], :password=>interpolated['password']}} |
|
58 |
+ parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(interpolated['from'])}+to+#{URI.encode(interpolated['to'])}", auth_options |
|
59 |
+ fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(interpolated['end_date'])}\\3#{date_to_unix_epoch(interpolated['start_date'])}" |
|
60 | 60 |
fare = HTTParty.get fare_request, auth_options |
61 | 61 |
|
62 | 62 |
if fare["warnings"] |
@@ -64,7 +64,7 @@ module Agents |
||
64 | 64 |
else |
65 | 65 |
event = fare["results"].min {|a,b| a["cost"] <=> b["cost"]} |
66 | 66 |
event["date"] = Time.at(event["date"]).to_date.httpdate[0..15] |
67 |
- event["route"] = "#{options['from']} to #{options['to']}" |
|
67 |
+ event["route"] = "#{interpolated['from']} to #{interpolated['to']}" |
|
68 | 68 |
create_event :payload => event |
69 | 69 |
end |
70 | 70 |
end |
@@ -72,7 +72,7 @@ module Agents |
||
72 | 72 |
|
73 | 73 |
private |
74 | 74 |
def request_url |
75 |
- "https://basecamp.com/#{URI.encode(self.service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(options[:project_id].to_s)}/events.json" |
|
75 |
+ "https://basecamp.com/#{URI.encode(self.service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
76 | 76 |
end |
77 | 77 |
|
78 | 78 |
def request_options |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class DataOutputAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
|
7 | 5 |
description do |
@@ -52,6 +50,7 @@ module Agents |
||
52 | 50 |
unless options['secrets'].is_a?(Array) && options['secrets'].length > 0 |
53 | 51 |
errors.add(:base, "Please specify one or more secrets for 'authenticating' incoming feed requests") |
54 | 52 |
end |
53 |
+ |
|
55 | 54 |
unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0 |
56 | 55 |
errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working") |
57 | 56 |
end |
@@ -62,27 +61,27 @@ module Agents |
||
62 | 61 |
end |
63 | 62 |
|
64 | 63 |
def events_to_show |
65 |
- (options['events_to_show'].presence || 40).to_i |
|
64 |
+ (interpolated['events_to_show'].presence || 40).to_i |
|
66 | 65 |
end |
67 | 66 |
|
68 | 67 |
def feed_ttl |
69 |
- (options['ttl'].presence || 60).to_i |
|
68 |
+ (interpolated['ttl'].presence || 60).to_i |
|
70 | 69 |
end |
71 | 70 |
|
72 | 71 |
def feed_title |
73 |
- options['template']['title'].presence || "#{name} Event Feed" |
|
72 |
+ interpolated['template']['title'].presence || "#{name} Event Feed" |
|
74 | 73 |
end |
75 | 74 |
|
76 | 75 |
def feed_link |
77 |
- options['template']['link'].presence || "https://#{ENV['DOMAIN']}" |
|
76 |
+ interpolated['template']['link'].presence || "https://#{ENV['DOMAIN']}" |
|
78 | 77 |
end |
79 | 78 |
|
80 | 79 |
def feed_description |
81 |
- options['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent" |
|
80 |
+ interpolated['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent" |
|
82 | 81 |
end |
83 | 82 |
|
84 | 83 |
def receive_web_request(params, method, format) |
85 |
- if options['secrets'].include?(params['secret']) |
|
84 |
+ if interpolated['secrets'].include?(params['secret']) |
|
86 | 85 |
items = received_events.order('id desc').limit(events_to_show).map do |event| |
87 | 86 |
interpolated = interpolate_options(options['template']['item'], event.payload) |
88 | 87 |
interpolated['guid'] = event.id |
@@ -25,7 +25,7 @@ module Agents |
||
25 | 25 |
def receive(incoming_events) |
26 | 26 |
incoming_events.each do |event| |
27 | 27 |
log "Sending digest mail to #{user.email} with event #{event.id}" |
28 |
- SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => [present(event.payload)]) |
|
28 |
+ SystemMailer.delay.send_message(:to => user.email, :subject => interpolated(event.payload)['subject'], :headline => interpolated(event.payload)['headline'], :groups => [present(event.payload)]) |
|
29 | 29 |
end |
30 | 30 |
end |
31 | 31 |
end |
@@ -37,7 +37,7 @@ module Agents |
||
37 | 37 |
ids = self.memory['events'].join(",") |
38 | 38 |
groups = self.memory['queue'].map { |payload| present(payload) } |
39 | 39 |
log "Sending digest mail to #{user.email} with events [#{ids}]" |
40 |
- SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => groups) |
|
40 |
+ SystemMailer.delay.send_message(:to => user.email, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups) |
|
41 | 41 |
self.memory['queue'] = [] |
42 | 42 |
self.memory['events'] = [] |
43 | 43 |
end |
@@ -1,6 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class EventFormattingAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 | 3 |
cannot_be_scheduled! |
5 | 4 |
|
6 | 5 |
description <<-MD |
@@ -81,7 +80,7 @@ module Agents |
||
81 | 80 |
after_save :clear_matchers |
82 | 81 |
|
83 | 82 |
def validate_options |
84 |
- errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present? |
|
83 |
+ errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? && options['mode'].present? && options['skip_agent'].present? && options['skip_created_at'].present? |
|
85 | 84 |
|
86 | 85 |
validate_matchers |
87 | 86 |
end |
@@ -105,11 +104,12 @@ module Agents |
||
105 | 104 |
|
106 | 105 |
def receive(incoming_events) |
107 | 106 |
incoming_events.each do |event| |
108 |
- formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {} |
|
109 | 107 |
payload = perform_matching(event.payload) |
110 |
- formatted_event.merge! interpolate_options(options['instructions'], payload) |
|
111 |
- formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
|
112 |
- formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
|
108 |
+ opts = interpolated(payload) |
|
109 |
+ formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {} |
|
110 |
+ formatted_event.merge! opts['instructions'] |
|
111 |
+ formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless opts['skip_agent'].to_s == "true" |
|
112 |
+ formatted_event['created_at'] = event.created_at unless opts['skip_created_at'].to_s == "true" |
|
113 | 113 |
create_event :payload => formatted_event |
114 | 114 |
end |
115 | 115 |
end |
@@ -29,7 +29,7 @@ module Agents |
||
29 | 29 |
MD |
30 | 30 |
|
31 | 31 |
def working? |
32 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
32 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
33 | 33 |
end |
34 | 34 |
|
35 | 35 |
def default_options |
@@ -90,10 +90,10 @@ module Agents |
||
90 | 90 |
end |
91 | 91 |
|
92 | 92 |
def each_entry |
93 |
- patterns = options['patterns'] |
|
93 |
+ patterns = interpolated['patterns'] |
|
94 | 94 |
|
95 | 95 |
after = |
96 |
- if str = options['after'] |
|
96 |
+ if str = interpolated['after'] |
|
97 | 97 |
Time.parse(str) |
98 | 98 |
else |
99 | 99 |
Time.at(0) |
@@ -174,7 +174,7 @@ module Agents |
||
174 | 174 |
end |
175 | 175 |
|
176 | 176 |
def base_uri |
177 |
- @base_uri ||= URI(options['url']) |
|
177 |
+ @base_uri ||= URI(interpolated['url']) |
|
178 | 178 |
end |
179 | 179 |
|
180 | 180 |
def saving_entries |
@@ -26,7 +26,7 @@ module Agents |
||
26 | 26 |
end |
27 | 27 |
|
28 | 28 |
def working? |
29 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
29 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
30 | 30 |
end |
31 | 31 |
|
32 | 32 |
def validate_options |
@@ -36,13 +36,13 @@ module Agents |
||
36 | 36 |
end |
37 | 37 |
|
38 | 38 |
def register_growl |
39 |
- @growler = Growl.new options['growl_server'], options['growl_app_name'], "GNTP" |
|
40 |
- @growler.password = options['growl_password'] |
|
41 |
- @growler.add_notification options['growl_notification_name'] |
|
39 |
+ @growler = Growl.new interpolated['growl_server'], interpolated['growl_app_name'], "GNTP" |
|
40 |
+ @growler.password = interpolated['growl_password'] |
|
41 |
+ @growler.add_notification interpolated['growl_notification_name'] |
|
42 | 42 |
end |
43 | 43 |
|
44 | 44 |
def notify_growl(subject, message) |
45 |
- @growler.notify(options['growl_notification_name'],subject,message) |
|
45 |
+ @growler.notify(interpolated['growl_notification_name'], subject, message) |
|
46 | 46 |
end |
47 | 47 |
|
48 | 48 |
def receive(incoming_events) |
@@ -51,7 +51,7 @@ module Agents |
||
51 | 51 |
message = (event.payload['message'] || event.payload['text']).to_s |
52 | 52 |
subject = event.payload['subject'].to_s |
53 | 53 |
if message.present? && subject.present? |
54 |
- log "Sending Growl notification '#{subject}': '#{message}' to #{options['growl_server']} with event #{event.id}" |
|
54 |
+ log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event.payload)['growl_server']} with event #{event.id}" |
|
55 | 55 |
notify_growl(subject,message) |
56 | 56 |
else |
57 | 57 |
log "Event #{event.id} not sent, message and subject expected" |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
cannot_create_events! |
7 | 5 |
|
@@ -42,9 +40,9 @@ module Agents |
||
42 | 40 |
end |
43 | 41 |
|
44 | 42 |
def receive(incoming_events) |
45 |
- client = HipChat::Client.new(options[:auth_token]) |
|
43 |
+ client = HipChat::Client.new(interpolated[:auth_token]) |
|
46 | 44 |
incoming_events.each do |event| |
47 |
- mo = interpolate_options options, event.payload |
|
45 |
+ mo = interpolated(event.payload) |
|
48 | 46 |
client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
49 | 47 |
end |
50 | 48 |
end |
@@ -2,8 +2,6 @@ require 'rturk' |
||
2 | 2 |
|
3 | 3 |
module Agents |
4 | 4 |
class HumanTaskAgent < Agent |
5 |
- include LiquidInterpolatable |
|
6 |
- |
|
7 | 5 |
default_schedule "every_10m" |
8 | 6 |
|
9 | 7 |
description <<-MD |
@@ -204,20 +202,20 @@ module Agents |
||
204 | 202 |
end |
205 | 203 |
|
206 | 204 |
def working? |
207 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
205 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
208 | 206 |
end |
209 | 207 |
|
210 | 208 |
def check |
211 | 209 |
review_hits |
212 | 210 |
|
213 |
- if options['trigger_on'] == "schedule" && (memory['last_schedule'] || 0) <= Time.now.to_i - options['submission_period'].to_i * 60 * 60 |
|
211 |
+ if interpolated['trigger_on'] == "schedule" && (memory['last_schedule'] || 0) <= Time.now.to_i - interpolated['submission_period'].to_i * 60 * 60 |
|
214 | 212 |
memory['last_schedule'] = Time.now.to_i |
215 | 213 |
create_basic_hit |
216 | 214 |
end |
217 | 215 |
end |
218 | 216 |
|
219 | 217 |
def receive(incoming_events) |
220 |
- if options['trigger_on'] == "event" |
|
218 |
+ if interpolated['trigger_on'] == "event" |
|
221 | 219 |
incoming_events.each do |event| |
222 | 220 |
create_basic_hit event |
223 | 221 |
end |
@@ -227,11 +225,11 @@ module Agents |
||
227 | 225 |
protected |
228 | 226 |
|
229 | 227 |
def take_majority? |
230 |
- options['combination_mode'] == "take_majority" || options['take_majority'] == "true" |
|
228 |
+ interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" |
|
231 | 229 |
end |
232 | 230 |
|
233 | 231 |
def create_poll? |
234 |
- options['combination_mode'] == "poll" |
|
232 |
+ interpolated['combination_mode'] == "poll" |
|
235 | 233 |
end |
236 | 234 |
|
237 | 235 |
def event_for_hit(hit_id) |
@@ -367,7 +365,7 @@ module Agents |
||
367 | 365 |
end |
368 | 366 |
|
369 | 367 |
def all_questions_are_numeric? |
370 |
- options['hit']['questions'].all? do |question| |
|
368 |
+ interpolated['hit']['questions'].all? do |question| |
|
371 | 369 |
question['selections'].all? do |selection| |
372 | 370 |
selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
373 | 371 |
end |
@@ -111,7 +111,7 @@ module Agents |
||
111 | 111 |
} |
112 | 112 |
|
113 | 113 |
def working? |
114 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
114 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
115 | 115 |
end |
116 | 116 |
|
117 | 117 |
def default_options |
@@ -240,7 +240,7 @@ module Agents |
||
240 | 240 |
matched_part = nil |
241 | 241 |
matches = {} |
242 | 242 |
|
243 |
- options['conditions'].all? { |key, value| |
|
243 |
+ interpolated['conditions'].all? { |key, value| |
|
244 | 244 |
case key |
245 | 245 |
when 'subject' |
246 | 246 |
value.present? or next true |
@@ -308,7 +308,7 @@ module Agents |
||
308 | 308 |
notified << mail.message_id if mail.message_id |
309 | 309 |
end |
310 | 310 |
|
311 |
- if options['mark_as_read'] |
|
311 |
+ if interpolated['mark_as_read'] |
|
312 | 312 |
log 'Marking as read' |
313 | 313 |
mail.mark_as_read |
314 | 314 |
end |
@@ -322,14 +322,14 @@ module Agents |
||
322 | 322 |
end |
323 | 323 |
|
324 | 324 |
def each_unread_mail |
325 |
- host, port, ssl, username = options.values_at(:host, :port, :ssl, :username) |
|
325 |
+ host, port, ssl, username = interpolated.values_at(:host, :port, :ssl, :username) |
|
326 | 326 |
|
327 | 327 |
log "Connecting to #{host}#{':%d' % port if port}#{' via SSL' if ssl}" |
328 | 328 |
Client.open(host, Integer(port), ssl) { |imap| |
329 | 329 |
log "Logging in as #{username}" |
330 |
- imap.login(username, options[:password]) |
|
330 |
+ imap.login(username, interpolated[:password]) |
|
331 | 331 |
|
332 |
- options['folders'].each { |folder| |
|
332 |
+ interpolated['folders'].each { |folder| |
|
333 | 333 |
log "Selecting the folder: %s" % folder |
334 | 334 |
|
335 | 335 |
imap.select(folder) |
@@ -351,7 +351,7 @@ module Agents |
||
351 | 351 |
end |
352 | 352 |
|
353 | 353 |
def mime_types |
354 |
- options['mime_types'] || %w[text/plain text/enriched text/html] |
|
354 |
+ interpolated['mime_types'] || %w[text/plain text/enriched text/html] |
|
355 | 355 |
end |
356 | 356 |
|
357 | 357 |
private |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class JabberAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
cannot_create_events! |
7 | 5 |
|
@@ -30,12 +28,12 @@ module Agents |
||
30 | 28 |
end |
31 | 29 |
|
32 | 30 |
def working? |
33 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
31 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
34 | 32 |
end |
35 | 33 |
|
36 | 34 |
def receive(incoming_events) |
37 | 35 |
incoming_events.each do |event| |
38 |
- log "Sending IM to #{options['jabber_receiver']} with event #{event.id}" |
|
36 |
+ log "Sending IM to #{interpolated['jabber_receiver']} with event #{event.id}" |
|
39 | 37 |
deliver body(event) |
40 | 38 |
end |
41 | 39 |
end |
@@ -45,15 +43,15 @@ module Agents |
||
45 | 43 |
end |
46 | 44 |
|
47 | 45 |
def deliver(text) |
48 |
- client.send Jabber::Message::new(options['jabber_receiver'], text).set_type(:chat) |
|
46 |
+ client.send Jabber::Message::new(interpolated['jabber_receiver'], text).set_type(:chat) |
|
49 | 47 |
end |
50 | 48 |
|
51 | 49 |
private |
52 | 50 |
|
53 | 51 |
def client |
54 |
- Jabber::Client.new(Jabber::JID::new(options['jabber_sender'])).tap do |sender| |
|
55 |
- sender.connect(options['jabber_server'], (options['jabber_port'] || '5222')) |
|
56 |
- sender.auth(options['jabber_password']) |
|
52 |
+ Jabber::Client.new(Jabber::JID::new(interpolated['jabber_sender'])).tap do |sender| |
|
53 |
+ sender.connect(interpolated['jabber_server'], interpolated['jabber_port'] || '5222') |
|
54 |
+ sender.auth interpolated['jabber_password'] |
|
57 | 55 |
end |
58 | 56 |
end |
59 | 57 |
|
@@ -62,7 +60,7 @@ module Agents |
||
62 | 60 |
end |
63 | 61 |
|
64 | 62 |
def body(event) |
65 |
- interpolate_string(options['message'], event.payload) |
|
63 |
+ interpolated(event.payload)['message'] |
|
66 | 64 |
end |
67 | 65 |
end |
68 | 66 |
end |
@@ -35,12 +35,12 @@ module Agents |
||
35 | 35 |
def working? |
36 | 36 |
return false if recent_error_logs? |
37 | 37 |
|
38 |
- if options['expected_update_period_in_days'].present? |
|
39 |
- return false unless event_created_within?(options['expected_update_period_in_days']) |
|
38 |
+ if interpolated['expected_update_period_in_days'].present? |
|
39 |
+ return false unless event_created_within?(interpolated['expected_update_period_in_days']) |
|
40 | 40 |
end |
41 | 41 |
|
42 |
- if options['expected_receive_period_in_days'].present? |
|
43 |
- return false unless last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago |
|
42 |
+ if interpolated['expected_receive_period_in_days'].present? |
|
43 |
+ return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago |
|
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
true |
@@ -92,7 +92,7 @@ module Agents |
||
92 | 92 |
|
93 | 93 |
context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json } |
94 | 94 |
context["getIncomingEvents"] = lambda { |a| incoming_events.to_json } |
95 |
- context["getOptions"] = lambda { |a, x| options.to_json } |
|
95 |
+ context["getOptions"] = lambda { |a, x| interpolated.to_json } |
|
96 | 96 |
context["doLog"] = lambda { |a, x| log x } |
97 | 97 |
context["doError"] = lambda { |a, x| error x } |
98 | 98 |
context["getMemory"] = lambda do |a, x, y| |
@@ -112,12 +112,12 @@ module Agents |
||
112 | 112 |
if cred |
113 | 113 |
credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };' |
114 | 114 |
else |
115 |
- options['code'] |
|
115 |
+ interpolated['code'] |
|
116 | 116 |
end |
117 | 117 |
end |
118 | 118 |
|
119 | 119 |
def credential_referenced_by_code |
120 |
- options['code'] =~ /\Acredential:(.*)\Z/ && $1 |
|
120 |
+ interpolated['code'] =~ /\Acredential:(.*)\Z/ && $1 |
|
121 | 121 |
end |
122 | 122 |
|
123 | 123 |
def setup_javascript |
@@ -56,7 +56,7 @@ module Agents |
||
56 | 56 |
end |
57 | 57 |
|
58 | 58 |
def working? |
59 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
59 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
60 | 60 |
end |
61 | 61 |
|
62 | 62 |
def check |
@@ -81,14 +81,14 @@ module Agents |
||
81 | 81 |
|
82 | 82 |
private |
83 | 83 |
def request_url(jql, start_at) |
84 |
- "#{options[:jira_url]}/rest/api/2/search?jql=#{CGI::escape(jql)}&fields=*all&startAt=#{start_at}" |
|
84 |
+ "#{interpolated[:jira_url]}/rest/api/2/search?jql=#{CGI::escape(jql)}&fields=*all&startAt=#{start_at}" |
|
85 | 85 |
end |
86 | 86 |
|
87 | 87 |
def request_options |
88 | 88 |
ropts = {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}} |
89 | 89 |
|
90 |
- if !options[:username].empty? |
|
91 |
- ropts = ropts.merge({:basic_auth => {:username =>options[:username], :password=>options[:password]}}) |
|
90 |
+ if !interpolated[:username].empty? |
|
91 |
+ ropts = ropts.merge({:basic_auth => {:username =>interpolated[:username], :password=>interpolated[:password]}}) |
|
92 | 92 |
end |
93 | 93 |
|
94 | 94 |
ropts |
@@ -121,10 +121,10 @@ module Agents |
||
121 | 121 |
|
122 | 122 |
jql = "" |
123 | 123 |
|
124 |
- if !options[:jql].empty? && since |
|
125 |
- jql = "(#{options[:jql]}) and updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" |
|
124 |
+ if !interpolated[:jql].empty? && since |
|
125 |
+ jql = "(#{interpolated[:jql]}) and updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" |
|
126 | 126 |
else |
127 |
- jql = options[:jql] if !options[:jql].empty? |
|
127 |
+ jql = interpolated[:jql] if !interpolated[:jql].empty? |
|
128 | 128 |
jql = "updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" if since |
129 | 129 |
end |
130 | 130 |
|
@@ -142,7 +142,7 @@ module Agents |
||
142 | 142 |
raise RuntimeError.new("There is no progress while fetching issues") |
143 | 143 |
end |
144 | 144 |
|
145 |
- if Time.now > start_time + options['timeout'].to_i * 60 |
|
145 |
+ if Time.now > start_time + interpolated['timeout'].to_i * 60 |
|
146 | 146 |
raise RuntimeError.new("Timeout exceeded while fetching issues") |
147 | 147 |
end |
148 | 148 |
|
@@ -68,13 +68,13 @@ module Agents |
||
68 | 68 |
|
69 | 69 |
def validate_options |
70 | 70 |
unless options['uri'].present? && |
71 |
- options['topic'].present? |
|
71 |
+ options['topic'].present? |
|
72 | 72 |
errors.add(:base, "topic and uri are required") |
73 | 73 |
end |
74 | 74 |
end |
75 | 75 |
|
76 | 76 |
def working? |
77 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
77 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
78 | 78 |
end |
79 | 79 |
|
80 | 80 |
def default_options |
@@ -91,13 +91,13 @@ module Agents |
||
91 | 91 |
end |
92 | 92 |
|
93 | 93 |
def mqtt_client |
94 |
- @client ||= MQTT::Client.new(options['uri']) |
|
94 |
+ @client ||= MQTT::Client.new(interpolated['uri']) |
|
95 | 95 |
|
96 |
- if options['ssl'] |
|
97 |
- @client.ssl = options['ssl'].to_sym |
|
98 |
- @client.ca_file = options['ca_file'] |
|
99 |
- @client.cert_file = options['cert_file'] |
|
100 |
- @client.key_file = options['key_file'] |
|
96 |
+ if interpolated['ssl'] |
|
97 |
+ @client.ssl = interpolated['ssl'].to_sym |
|
98 |
+ @client.ca_file = interpolated['ca_file'] |
|
99 |
+ @client.cert_file = interpolated['cert_file'] |
|
100 |
+ @client.key_file = interpolated['key_file'] |
|
101 | 101 |
end |
102 | 102 |
|
103 | 103 |
@client |
@@ -106,7 +106,7 @@ module Agents |
||
106 | 106 |
def receive(incoming_events) |
107 | 107 |
mqtt_client.connect do |c| |
108 | 108 |
incoming_events.each do |event| |
109 |
- c.publish(options['topic'], payload) |
|
109 |
+ c.publish(interpolated(event.payload)['topic'], event.payload) |
|
110 | 110 |
end |
111 | 111 |
|
112 | 112 |
c.disconnect |
@@ -117,8 +117,8 @@ module Agents |
||
117 | 117 |
def check |
118 | 118 |
mqtt_client.connect do |c| |
119 | 119 |
|
120 |
- Timeout::timeout((options['max_read_time'].presence || 15).to_i) { |
|
121 |
- c.get(options['topic']) do |topic, message| |
|
120 |
+ Timeout::timeout((interpolated['max_read_time'].presence || 15).to_i) { |
|
121 |
+ c.get(interpolated['topic']) do |topic, message| |
|
122 | 122 |
|
123 | 123 |
# A lot of services generate JSON. Try that first |
124 | 124 |
payload = JSON.parse(message) rescue message |
@@ -2,8 +2,6 @@ require 'pp' |
||
2 | 2 |
|
3 | 3 |
module Agents |
4 | 4 |
class PeakDetectorAgent < Agent |
5 |
- include LiquidInterpolatable |
|
6 |
- |
|
7 | 5 |
cannot_be_scheduled! |
8 | 6 |
|
9 | 7 |
description <<-MD |
@@ -45,7 +43,7 @@ module Agents |
||
45 | 43 |
end |
46 | 44 |
|
47 | 45 |
def working? |
48 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
46 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
49 | 47 |
end |
50 | 48 |
|
51 | 49 |
def receive(incoming_events) |
@@ -69,7 +67,7 @@ module Agents |
||
69 | 67 |
if newest_value > average_value + std_multiple * standard_deviation |
70 | 68 |
memory['peaks'][group] << newest_time |
71 | 69 |
memory['peaks'][group].reject! { |p| p <= newest_time - window_duration } |
72 |
- create_event :payload => { 'message' => interpolate_string(options['message'], event.payload), 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
70 |
+ create_event :payload => { 'message' => interpolated(event.payload)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
73 | 71 |
end |
74 | 72 |
end |
75 | 73 |
end |
@@ -94,33 +92,33 @@ module Agents |
||
94 | 92 |
end |
95 | 93 |
|
96 | 94 |
def window_duration |
97 |
- if options['window_duration'].present? # The older option |
|
98 |
- options['window_duration'].to_i |
|
95 |
+ if interpolated['window_duration'].present? # The older option |
|
96 |
+ interpolated['window_duration'].to_i |
|
99 | 97 |
else |
100 |
- (options['window_duration_in_days'] || 14).to_f.days |
|
98 |
+ (interpolated['window_duration_in_days'] || 14).to_f.days |
|
101 | 99 |
end |
102 | 100 |
end |
103 | 101 |
|
104 | 102 |
def std_multiple |
105 |
- (options['std_multiple'] || 3).to_f |
|
103 |
+ (interpolated['std_multiple'] || 3).to_f |
|
106 | 104 |
end |
107 | 105 |
|
108 | 106 |
def peak_spacing |
109 |
- if options['peak_spacing'].present? # The older option |
|
110 |
- options['peak_spacing'].to_i |
|
107 |
+ if interpolated['peak_spacing'].present? # The older option |
|
108 |
+ interpolated['peak_spacing'].to_i |
|
111 | 109 |
else |
112 |
- (options['min_peak_spacing_in_days'] || 2).to_f.days |
|
110 |
+ (interpolated['min_peak_spacing_in_days'] || 2).to_f.days |
|
113 | 111 |
end |
114 | 112 |
end |
115 | 113 |
|
116 | 114 |
def group_for(event) |
117 |
- ((options['group_by_path'].present? && Utils.value_at(event.payload, options['group_by_path'])) || 'no_group') |
|
115 |
+ ((interpolated['group_by_path'].present? && Utils.value_at(event.payload, interpolated['group_by_path'])) || 'no_group') |
|
118 | 116 |
end |
119 | 117 |
|
120 | 118 |
def remember(group, event) |
121 | 119 |
memory['data'] ||= {} |
122 | 120 |
memory['data'][group] ||= [] |
123 |
- memory['data'][group] << [ Utils.value_at(event.payload, options['value_path']), event.created_at.to_i ] |
|
121 |
+ memory['data'][group] << [ Utils.value_at(event.payload, interpolated['value_path']), event.created_at.to_i ] |
|
124 | 122 |
cleanup group |
125 | 123 |
end |
126 | 124 |
|
@@ -27,15 +27,15 @@ module Agents |
||
27 | 27 |
end |
28 | 28 |
|
29 | 29 |
def working? |
30 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
30 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def method |
34 |
- (options['method'].presence || 'post').to_s.downcase |
|
34 |
+ (interpolated['method'].presence || 'post').to_s.downcase |
|
35 | 35 |
end |
36 | 36 |
|
37 | 37 |
def headers |
38 |
- options['headers'].presence || {} |
|
38 |
+ interpolated['headers'].presence || {} |
|
39 | 39 |
end |
40 | 40 |
|
41 | 41 |
def validate_options |
@@ -58,16 +58,16 @@ module Agents |
||
58 | 58 |
|
59 | 59 |
def receive(incoming_events) |
60 | 60 |
incoming_events.each do |event| |
61 |
- handle (options['payload'].presence || {}).merge(event.payload) |
|
61 |
+ handle (interpolated(event.payload)['payload'].presence || {}).merge(event.payload) |
|
62 | 62 |
end |
63 | 63 |
end |
64 | 64 |
|
65 | 65 |
def check |
66 |
- handle options['payload'].presence || {} |
|
66 |
+ handle interpolated['payload'].presence || {} |
|
67 | 67 |
end |
68 | 68 |
|
69 | 69 |
def generate_uri(params = nil) |
70 |
- uri = URI options[:post_url] |
|
70 |
+ uri = URI interpolated[:post_url] |
|
71 | 71 |
uri.query = URI.encode_www_form(Hash[URI.decode_www_form(uri.query || '')].merge(params)) if params |
72 | 72 |
uri |
73 | 73 |
end |
@@ -48,12 +48,12 @@ module Agents |
||
48 | 48 |
MD |
49 | 49 |
|
50 | 50 |
def check_url |
51 |
- stop_query = URI.encode(options["stops"].collect{|a| "&stops=#{a}"}.join) |
|
52 |
- "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=#{options["agency"]}#{stop_query}" |
|
51 |
+ stop_query = URI.encode(interpolated["stops"].collect{|a| "&stops=#{a}"}.join) |
|
52 |
+ "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=#{interpolated["agency"]}#{stop_query}" |
|
53 | 53 |
end |
54 | 54 |
|
55 | 55 |
def stops |
56 |
- options["stops"].collect{|a| a.split("|").last} |
|
56 |
+ interpolated["stops"].collect{|a| a.split("|").last} |
|
57 | 57 |
end |
58 | 58 |
|
59 | 59 |
def check |
@@ -65,7 +65,7 @@ module Agents |
||
65 | 65 |
predictions.each do |pr| |
66 | 66 |
parent = pr.parent.parent |
67 | 67 |
vals = {"routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"]} |
68 |
- if pr["minutes"] && pr["minutes"].to_i < options["alert_window_in_minutes"].to_i |
|
68 |
+ if pr["minutes"] && pr["minutes"].to_i < interpolated["alert_window_in_minutes"].to_i |
|
69 | 69 |
vals = vals.merge Hash.from_xml(pr.to_xml) |
70 | 70 |
if not_already_in_memory?(vals) |
71 | 71 |
create_event(:payload => vals) |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class PushbulletAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
cannot_create_events! |
7 | 5 |
|
@@ -49,10 +47,11 @@ module Agents |
||
49 | 47 |
end |
50 | 48 |
|
51 | 49 |
private |
50 |
+ |
|
52 | 51 |
def query_options(event) |
53 |
- mo = interpolate_options options, event.payload |
|
52 |
+ mo = interpolated(event.payload) |
|
54 | 53 |
{ |
55 |
- :basic_auth => {:username =>mo[:api_key], :password=>''}, |
|
54 |
+ :basic_auth => {:username => mo[:api_key], :password => ''}, |
|
56 | 55 |
:body => {:device_iden => mo[:device_id], :title => mo[:title], :body => mo[:body], :type => 'note'} |
57 | 56 |
} |
58 | 57 |
end |
@@ -19,13 +19,13 @@ module Agents |
||
19 | 19 |
Your event can provide any of the following optional parameters or you can provide defaults: |
20 | 20 |
|
21 | 21 |
* `device` - your user's device name to send the message directly to that device, rather than all of the user's devices |
22 |
- * `title` or `subject` - your notifications's title |
|
22 |
+ * `title` or `subject` - your notification's title |
|
23 | 23 |
* `url` - a supplementary URL to show with your message - `512` Character Limit |
24 | 24 |
* `url_title` - a title for your supplementary URL, otherwise just the URL is shown - `100` Character Limit |
25 | 25 |
* `priority` - send as `-1` to always send as a quiet notification, `0` is default, `1` to display as high-priority and bypass the user's quiet hours, or `2` for emergency priority: [Please read Pushover Docs on Emergency Priority](https://pushover.net/api#priority) |
26 | 26 |
* `sound` - the name of one of the sounds supported by device clients to override the user's default sound choice. [See PushOver docs for sound options.](https://pushover.net/api#sounds) |
27 |
- * `retry` - Requred for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30` |
|
28 |
- * `expire` - Requred for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400` |
|
27 |
+ * `retry` - Required for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30` |
|
28 |
+ * `expire` - Required for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400` |
|
29 | 29 |
|
30 | 30 |
Your event can also pass along a timestamp parameter: |
31 | 31 |
|
@@ -42,10 +42,10 @@ module Agents |
||
42 | 42 |
'title' => '', |
43 | 43 |
'url' => '', |
44 | 44 |
'url_title' => '', |
45 |
- 'priority' => 0, |
|
45 |
+ 'priority' => '0', |
|
46 | 46 |
'sound' => 'pushover', |
47 |
- 'retry' => 0, |
|
48 |
- 'expire' => 0, |
|
47 |
+ 'retry' => '0', |
|
48 |
+ 'expire' => '0', |
|
49 | 49 |
'expected_receive_period_in_days' => '1' |
50 | 50 |
} |
51 | 51 |
end |
@@ -58,50 +58,50 @@ module Agents |
||
58 | 58 |
|
59 | 59 |
def receive(incoming_events) |
60 | 60 |
incoming_events.each do |event| |
61 |
- message = (event.payload['message'].presence || event.payload['text'].presence || options['message']).to_s |
|
61 |
+ payload_interpolated = interpolated(event.payload) |
|
62 |
+ message = (event.payload['message'].presence || event.payload['text'].presence || payload_interpolated['message']).to_s |
|
62 | 63 |
if message.present? |
63 |
- post_params = { |
|
64 |
- 'token' => options['token'], |
|
65 |
- 'user' => options['user'], |
|
66 |
- 'message' => message |
|
67 |
- } |
|
64 |
+ post_params = { |
|
65 |
+ 'token' => payload_interpolated['token'], |
|
66 |
+ 'user' => payload_interpolated['user'], |
|
67 |
+ 'message' => message |
|
68 |
+ } |
|
68 | 69 |
|
69 |
- post_params['device'] = event.payload['device'].presence || options['device'] |
|
70 |
- post_params['title'] = event.payload['title'].presence || event.payload['subject'].presence || options['title'] |
|
70 |
+ post_params['device'] = event.payload['device'].presence || payload_interpolated['device'] |
|
71 |
+ post_params['title'] = event.payload['title'].presence || event.payload['subject'].presence || payload_interpolated['title'] |
|
71 | 72 |
|
72 |
- url = (event.payload['url'].presence || options['url'] || '').to_s |
|
73 |
- url = url.slice 0..512 |
|
74 |
- post_params['url'] = url |
|
73 |
+ url = (event.payload['url'].presence || payload_interpolated['url'] || '').to_s |
|
74 |
+ url = url.slice 0..512 |
|
75 |
+ post_params['url'] = url |
|
75 | 76 |
|
76 |
- url_title = (event.payload['url_title'].presence || options['url_title']).to_s |
|
77 |
- url_title = url_title.slice 0..100 |
|
78 |
- post_params['url_title'] = url_title |
|
77 |
+ url_title = (event.payload['url_title'].presence || payload_interpolated['url_title']).to_s |
|
78 |
+ url_title = url_title.slice 0..100 |
|
79 |
+ post_params['url_title'] = url_title |
|
79 | 80 |
|
80 |
- post_params['priority'] = (event.payload['priority'].presence || options['priority']).to_i |
|
81 |
+ post_params['priority'] = (event.payload['priority'].presence || payload_interpolated['priority']).to_i |
|
81 | 82 |
|
82 |
- if event.payload.has_key? 'timestamp' |
|
83 |
- post_params['timestamp'] = (event.payload['timestamp']).to_s |
|
84 |
- end |
|
83 |
+ if event.payload.has_key? 'timestamp' |
|
84 |
+ post_params['timestamp'] = (event.payload['timestamp']).to_s |
|
85 |
+ end |
|
85 | 86 |
|
86 |
- post_params['sound'] = (event.payload['sound'].presence || options['sound']).to_s |
|
87 |
+ post_params['sound'] = (event.payload['sound'].presence || payload_interpolated['sound']).to_s |
|
87 | 88 |
|
88 |
- post_params['retry'] = (event.payload['retry'].presence || options['retry']).to_i |
|
89 |
+ post_params['retry'] = (event.payload['retry'].presence || payload_interpolated['retry']).to_i |
|
89 | 90 |
|
90 |
- post_params['expire'] = (event.payload['expire'].presence || options['expire']).to_i |
|
91 |
+ post_params['expire'] = (event.payload['expire'].presence || payload_interpolated['expire']).to_i |
|
91 | 92 |
|
92 |
- send_notification(post_params) |
|
93 |
+ send_notification(post_params) |
|
93 | 94 |
end |
94 | 95 |
end |
95 | 96 |
end |
96 | 97 |
|
97 | 98 |
def working? |
98 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
99 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
99 | 100 |
end |
100 | 101 |
|
101 | 102 |
def send_notification(post_params) |
102 | 103 |
response = HTTParty.post(API_URL, :query => post_params) |
103 | 104 |
puts response |
104 | 105 |
end |
105 |
- |
|
106 | 106 |
end |
107 | 107 |
end |
@@ -34,13 +34,13 @@ module Agents |
||
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
def working? |
37 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
37 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
38 | 38 |
end |
39 | 39 |
|
40 | 40 |
def receive(incoming_events) |
41 | 41 |
anew = self.class.sentiment_hash |
42 | 42 |
incoming_events.each do |event| |
43 |
- Utils.values_at(event.payload, options['content']).each do |content| |
|
43 |
+ Utils.values_at(event.payload, interpolated['content']).each do |content| |
|
44 | 44 |
sent_values = sentiment_values anew, content |
45 | 45 |
create_event :payload => { 'content' => content, |
46 | 46 |
'valence' => sent_values[0], |
@@ -15,7 +15,8 @@ module Agents |
||
15 | 15 |
|
16 | 16 |
`expected_update_period_in_days` is used to determine if the Agent is working. |
17 | 17 |
|
18 |
- ShellCommandAgent can also act upon received events. These events may contain their own `path` and `command` values. If they do not, ShellCommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent to respond to events. |
|
18 |
+ ShellCommandAgent can also act upon received events. When receiving an event, this Agent's options can interpolate values from the incoming event. |
|
19 |
+ For example, your command could be defined as `{{cmd}}`, in which case the event's `cmd` property would be used. |
|
19 | 20 |
|
20 | 21 |
The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong. |
21 | 22 |
|
@@ -55,25 +56,25 @@ module Agents |
||
55 | 56 |
end |
56 | 57 |
|
57 | 58 |
def working? |
58 |
- Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
59 |
+ Agents::ShellCommandAgent.should_run? && event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
59 | 60 |
end |
60 | 61 |
|
61 | 62 |
def receive(incoming_events) |
62 | 63 |
incoming_events.each do |event| |
63 |
- handle(event.payload, event) |
|
64 |
+ handle(interpolated(event.payload), event) |
|
64 | 65 |
end |
65 | 66 |
end |
66 | 67 |
|
67 | 68 |
def check |
68 |
- handle(options) |
|
69 |
+ handle(interpolated) |
|
69 | 70 |
end |
70 | 71 |
|
71 | 72 |
private |
72 | 73 |
|
73 |
- def handle(opts = options, event = nil) |
|
74 |
+ def handle(opts, event = nil) |
|
74 | 75 |
if Agents::ShellCommandAgent.should_run? |
75 |
- command = opts['command'] || options['command'] |
|
76 |
- path = opts['path'] || options['path'] |
|
76 |
+ command = opts['command'] |
|
77 |
+ path = opts['path'] |
|
77 | 78 |
|
78 | 79 |
result, errors, exit_status = run_command(path, command) |
79 | 80 |
|
@@ -1,6 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class SlackAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 | 3 |
cannot_be_scheduled! |
5 | 4 |
cannot_create_events! |
6 | 5 |
|
@@ -45,20 +44,20 @@ module Agents |
||
45 | 44 |
end |
46 | 45 |
|
47 | 46 |
def webhook |
48 |
- options[:webhook].presence || DEFAULT_WEBHOOK |
|
47 |
+ interpolated[:webhook].presence || DEFAULT_WEBHOOK |
|
49 | 48 |
end |
50 | 49 |
|
51 | 50 |
def username |
52 |
- options[:username].presence || DEFAULT_USERNAME |
|
51 |
+ interpolated[:username].presence || DEFAULT_USERNAME |
|
53 | 52 |
end |
54 | 53 |
|
55 | 54 |
def slack_notifier |
56 |
- @slack_notifier ||= Slack::Notifier.new(options[:team_name], options[:auth_token], webhook, username: username) |
|
55 |
+ @slack_notifier ||= Slack::Notifier.new(interpolated[:team_name], interpolated[:auth_token], webhook, username: username) |
|
57 | 56 |
end |
58 | 57 |
|
59 | 58 |
def receive(incoming_events) |
60 | 59 |
incoming_events.each do |event| |
61 |
- opts = interpolate_options options, event.payload |
|
60 |
+ opts = interpolated(event.payload) |
|
62 | 61 |
slack_notifier.ping opts[:message], channel: opts[:channel], username: opts[:username] |
63 | 62 |
end |
64 | 63 |
end |
@@ -35,7 +35,7 @@ module Agents |
||
35 | 35 |
end |
36 | 36 |
|
37 | 37 |
def url |
38 |
- options['url'] |
|
38 |
+ interpolated['url'] |
|
39 | 39 |
end |
40 | 40 |
|
41 | 41 |
def check |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class TranslationAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
|
7 | 5 |
description <<-MD |
@@ -30,7 +28,7 @@ module Agents |
||
30 | 28 |
end |
31 | 29 |
|
32 | 30 |
def working? |
33 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
31 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
34 | 32 |
end |
35 | 33 |
|
36 | 34 |
def translate(text, to, access_token) |
@@ -61,16 +59,16 @@ module Agents |
||
61 | 59 |
|
62 | 60 |
def receive(incoming_events) |
63 | 61 |
auth_uri = URI "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13" |
64 |
- response = postform auth_uri, :client_id => options['client_id'], |
|
65 |
- :client_secret => options['client_secret'], |
|
62 |
+ response = postform auth_uri, :client_id => interpolated['client_id'], |
|
63 |
+ :client_secret => interpolated['client_secret'], |
|
66 | 64 |
:scope => "http://api.microsofttranslator.com", |
67 | 65 |
:grant_type => "client_credentials" |
68 | 66 |
access_token = JSON.parse(response.body)["access_token"] |
69 | 67 |
incoming_events.each do |event| |
70 | 68 |
translated_event = {} |
71 |
- options['content'].each_pair do |key, value| |
|
72 |
- to_be_translated = interpolate_string(value, event.payload) |
|
73 |
- translated_event[key] = translate(to_be_translated.first, options['to'], access_token) |
|
69 |
+ opts = interpolated(event.payload) |
|
70 |
+ opts['content'].each_pair do |key, value| |
|
71 |
+ translated_event[key] = translate(value.first, opts['to'], access_token) |
|
74 | 72 |
end |
75 | 73 |
create_event :payload => translated_event |
76 | 74 |
end |
@@ -1,7 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class TriggerAgent < Agent |
3 |
- include LiquidInterpolatable |
|
4 |
- |
|
5 | 3 |
cannot_be_scheduled! |
6 | 4 |
|
7 | 5 |
VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value] |
@@ -30,7 +28,7 @@ module Agents |
||
30 | 28 |
|
31 | 29 |
def validate_options |
32 | 30 |
unless options['expected_receive_period_in_days'].present? && options['rules'].present? && |
33 |
- options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? } |
|
31 |
+ options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? } |
|
34 | 32 |
errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required") |
35 | 33 |
end |
36 | 34 |
|
@@ -53,12 +51,15 @@ module Agents |
||
53 | 51 |
end |
54 | 52 |
|
55 | 53 |
def working? |
56 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
54 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
57 | 55 |
end |
58 | 56 |
|
59 | 57 |
def receive(incoming_events) |
60 | 58 |
incoming_events.each do |event| |
61 |
- match = options['rules'].all? do |rule| |
|
59 |
+ |
|
60 |
+ opts = interpolated(event.payload) |
|
61 |
+ |
|
62 |
+ match = opts['rules'].all? do |rule| |
|
62 | 63 |
value_at_path = Utils.value_at(event['payload'], rule['path']) |
63 | 64 |
rule_values = rule['value'] |
64 | 65 |
rule_values = [rule_values] unless rule_values.is_a?(Array) |
@@ -90,9 +91,9 @@ module Agents |
||
90 | 91 |
if match |
91 | 92 |
if keep_event? |
92 | 93 |
payload = event.payload.dup |
93 |
- payload['message'] = interpolate_string(options['message'], event.payload) if options['message'].present? |
|
94 |
+ payload['message'] = opts['message'] if opts['message'].present? |
|
94 | 95 |
else |
95 |
- payload = { 'message' => interpolate_string(options['message'], event.payload) } |
|
96 |
+ payload = { 'message' => opts['message'] } |
|
96 | 97 |
end |
97 | 98 |
|
98 | 99 |
create_event :payload => payload |
@@ -101,7 +102,7 @@ module Agents |
||
101 | 102 |
end |
102 | 103 |
|
103 | 104 |
def keep_event? |
104 |
- options['keep_event'] == 'true' |
|
105 |
+ interpolated['keep_event'] == 'true' |
|
105 | 106 |
end |
106 | 107 |
end |
107 | 108 |
end |
@@ -39,18 +39,18 @@ module Agents |
||
39 | 39 |
end |
40 | 40 |
|
41 | 41 |
def receive(incoming_events) |
42 |
- @client = Twilio::REST::Client.new options['account_sid'], options['auth_token'] |
|
42 |
+ @client = Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] |
|
43 | 43 |
memory['pending_calls'] ||= {} |
44 | 44 |
incoming_events.each do |event| |
45 | 45 |
message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
46 | 46 |
if message.present? |
47 |
- if options['receive_call'].to_s == 'true' |
|
47 |
+ if interpolated(event.payload)['receive_call'].to_s == 'true' |
|
48 | 48 |
secret = SecureRandom.hex 3 |
49 | 49 |
memory['pending_calls'][secret] = message |
50 | 50 |
make_call secret |
51 | 51 |
end |
52 | 52 |
|
53 |
- if options['receive_text'].to_s == 'true' |
|
53 |
+ if interpolated(event.payload)['receive_text'].to_s == 'true' |
|
54 | 54 |
message = message.slice 0..160 |
55 | 55 |
send_message message |
56 | 56 |
end |
@@ -59,19 +59,19 @@ module Agents |
||
59 | 59 |
end |
60 | 60 |
|
61 | 61 |
def working? |
62 |
- last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
62 |
+ last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
63 | 63 |
end |
64 | 64 |
|
65 | 65 |
def send_message(message) |
66 |
- @client.account.sms.messages.create :from => options['sender_cell'], |
|
67 |
- :to => options['receiver_cell'], |
|
66 |
+ @client.account.sms.messages.create :from => interpolated['sender_cell'], |
|
67 |
+ :to => interpolated['receiver_cell'], |
|
68 | 68 |
:body => message |
69 | 69 |
end |
70 | 70 |
|
71 | 71 |
def make_call(secret) |
72 |
- @client.account.calls.create :from => options['sender_cell'], |
|
73 |
- :to => options['receiver_cell'], |
|
74 |
- :url => post_url(options['server_url'], secret) |
|
72 |
+ @client.account.calls.create :from => interpolated['sender_cell'], |
|
73 |
+ :to => interpolated['receiver_cell'], |
|
74 |
+ :url => post_url(interpolated['server_url'], secret) |
|
75 | 75 |
end |
76 | 76 |
|
77 | 77 |
def post_url(server_url, secret) |
@@ -3,7 +3,6 @@ require "twitter" |
||
3 | 3 |
module Agents |
4 | 4 |
class TwitterPublishAgent < Agent |
5 | 5 |
include TwitterConcern |
6 |
- include LiquidInterpolatable |
|
7 | 6 |
|
8 | 7 |
cannot_be_scheduled! |
9 | 8 |
|
@@ -22,7 +21,7 @@ module Agents |
||
22 | 21 |
end |
23 | 22 |
|
24 | 23 |
def working? |
25 |
- event_created_within?(options['expected_update_period_in_days']) && most_recent_event && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
24 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && most_recent_event && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
26 | 25 |
end |
27 | 26 |
|
28 | 27 |
def default_options |
@@ -38,7 +37,7 @@ module Agents |
||
38 | 37 |
incoming_events = incoming_events.first(20) |
39 | 38 |
end |
40 | 39 |
incoming_events.each do |event| |
41 |
- tweet_text = interpolate_string(options['message'], event.payload) |
|
40 |
+ tweet_text = interpolated(event.payload)['message'] |
|
42 | 41 |
begin |
43 | 42 |
tweet = publish_tweet tweet_text |
44 | 43 |
create_event :payload => { |
@@ -61,7 +61,7 @@ module Agents |
||
61 | 61 |
end |
62 | 62 |
|
63 | 63 |
def working? |
64 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
64 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
65 | 65 |
end |
66 | 66 |
|
67 | 67 |
def default_options |
@@ -76,7 +76,7 @@ module Agents |
||
76 | 76 |
filter = lookup_filter(filter) |
77 | 77 |
|
78 | 78 |
if filter |
79 |
- if options['generate'] == "counts" |
|
79 |
+ if interpolated['generate'] == "counts" |
|
80 | 80 |
# Avoid memory pollution by reloading the Agent. |
81 | 81 |
agent = Agent.find(id) |
82 | 82 |
agent.memory['filter_counts'] ||= {} |
@@ -91,7 +91,7 @@ module Agents |
||
91 | 91 |
end |
92 | 92 |
|
93 | 93 |
def check |
94 |
- if options['generate'] == "counts" && memory['filter_counts'] && memory['filter_counts'].length > 0 |
|
94 |
+ if interpolated['generate'] == "counts" && memory['filter_counts'] && memory['filter_counts'].length > 0 |
|
95 | 95 |
memory['filter_counts'].each do |filter, count| |
96 | 96 |
create_event :payload => { 'filter' => filter, 'count' => count, 'time' => Time.now.to_i } |
97 | 97 |
end |
@@ -102,7 +102,7 @@ module Agents |
||
102 | 102 |
protected |
103 | 103 |
|
104 | 104 |
def lookup_filter(filter) |
105 |
- options['filters'].each do |known_filter| |
|
105 |
+ interpolated['filters'].each do |known_filter| |
|
106 | 106 |
if known_filter == filter |
107 | 107 |
return filter |
108 | 108 |
elsif known_filter.is_a?(Array) |
@@ -115,7 +115,7 @@ module Agents |
||
115 | 115 |
|
116 | 116 |
def remove_unused_keys!(agent, base) |
117 | 117 |
if agent.memory[base] |
118 |
- (agent.memory[base].keys - agent.options['filters'].map {|f| f.is_a?(Array) ? f.first.to_s : f.to_s }).each do |removed_key| |
|
118 |
+ (agent.memory[base].keys - agent.interpolated['filters'].map {|f| f.is_a?(Array) ? f.first.to_s : f.to_s }).each do |removed_key| |
|
119 | 119 |
agent.memory[base].delete(removed_key) |
120 | 120 |
end |
121 | 121 |
end |
@@ -47,7 +47,7 @@ module Agents |
||
47 | 47 |
default_schedule "every_1h" |
48 | 48 |
|
49 | 49 |
def working? |
50 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
50 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
51 | 51 |
end |
52 | 52 |
|
53 | 53 |
def default_options |
@@ -72,15 +72,15 @@ module Agents |
||
72 | 72 |
end |
73 | 73 |
|
74 | 74 |
def starting_at |
75 |
- if options[:starting_at].present? |
|
76 |
- Time.parse(options[:starting_at]) rescue created_at |
|
75 |
+ if interpolated[:starting_at].present? |
|
76 |
+ Time.parse(interpolated[:starting_at]) rescue created_at |
|
77 | 77 |
else |
78 | 78 |
created_at |
79 | 79 |
end |
80 | 80 |
end |
81 | 81 |
|
82 | 82 |
def include_retweets? |
83 |
- options[:include_retweets] != "false" |
|
83 |
+ interpolated[:include_retweets] != "false" |
|
84 | 84 |
end |
85 | 85 |
|
86 | 86 |
def check |
@@ -89,7 +89,7 @@ module Agents |
||
89 | 89 |
opts.merge! :since_id => since_id unless since_id.nil? |
90 | 90 |
|
91 | 91 |
# http://rdoc.info/gems/twitter/Twitter/REST/Timelines#user_timeline-instance_method |
92 |
- tweets = twitter.user_timeline(options['username'], opts) |
|
92 |
+ tweets = twitter.user_timeline(interpolated['username'], opts) |
|
93 | 93 |
|
94 | 94 |
tweets.each do |tweet| |
95 | 95 |
if tweet.created_at >= starting_at |
@@ -51,11 +51,11 @@ module Agents |
||
51 | 51 |
default_schedule "8pm" |
52 | 52 |
|
53 | 53 |
def working? |
54 |
- event_created_within?((options['expected_update_period_in_days'].presence || 2).to_i) && !recent_error_logs? |
|
54 |
+ event_created_within?((interpolated['expected_update_period_in_days'].presence || 2).to_i) && !recent_error_logs? |
|
55 | 55 |
end |
56 | 56 |
|
57 | 57 |
def key_setup? |
58 |
- options['api_key'].present? && options['api_key'] != "your-key" |
|
58 |
+ interpolated['api_key'].present? && interpolated['api_key'] != "your-key" |
|
59 | 59 |
end |
60 | 60 |
|
61 | 61 |
def default_options |
@@ -69,15 +69,15 @@ module Agents |
||
69 | 69 |
end |
70 | 70 |
|
71 | 71 |
def service |
72 |
- options["service"].presence || "wunderground" |
|
72 |
+ interpolated["service"].presence || "wunderground" |
|
73 | 73 |
end |
74 | 74 |
|
75 | 75 |
def which_day |
76 |
- (options["which_day"].presence || 1).to_i |
|
76 |
+ (interpolated["which_day"].presence || 1).to_i |
|
77 | 77 |
end |
78 | 78 |
|
79 | 79 |
def location |
80 |
- options["location"].presence || options["zipcode"] |
|
80 |
+ interpolated["location"].presence || interpolated["zipcode"] |
|
81 | 81 |
end |
82 | 82 |
|
83 | 83 |
def validate_options |
@@ -89,12 +89,12 @@ module Agents |
||
89 | 89 |
end |
90 | 90 |
|
91 | 91 |
def wunderground |
92 |
- Wunderground.new(options['api_key']).forecast_for(location)['forecast']['simpleforecast']['forecastday'] if key_setup? |
|
92 |
+ Wunderground.new(interpolated['api_key']).forecast_for(location)['forecast']['simpleforecast']['forecastday'] if key_setup? |
|
93 | 93 |
end |
94 | 94 |
|
95 | 95 |
def forecastio |
96 | 96 |
if key_setup? |
97 |
- ForecastIO.api_key = options['api_key'] |
|
97 |
+ ForecastIO.api_key = interpolated['api_key'] |
|
98 | 98 |
lat, lng = location.split(',') |
99 | 99 |
ForecastIO.forecast(lat,lng)['daily']['data'] |
100 | 100 |
end |
@@ -27,7 +27,7 @@ module Agents |
||
27 | 27 |
event_description do |
28 | 28 |
<<-MD |
29 | 29 |
The event payload is base on the value of the `payload_path` option, |
30 |
- which is set to `#{options['payload_path']}`. |
|
30 |
+ which is set to `#{interpolated['payload_path']}`. |
|
31 | 31 |
MD |
32 | 32 |
end |
33 | 33 |
|
@@ -40,7 +40,7 @@ module Agents |
||
40 | 40 |
def receive_web_request(params, method, format) |
41 | 41 |
secret = params.delete('secret') |
42 | 42 |
return ["Please use POST requests only", 401] unless method == "post" |
43 |
- return ["Not Authorized", 401] unless secret == options['secret'] |
|
43 |
+ return ["Not Authorized", 401] unless secret == interpolated['secret'] |
|
44 | 44 |
|
45 | 45 |
create_event(:payload => payload_for(params)) |
46 | 46 |
|
@@ -48,7 +48,7 @@ module Agents |
||
48 | 48 |
end |
49 | 49 |
|
50 | 50 |
def working? |
51 |
- event_created_within?(options['expected_receive_period_in_days']) && !recent_error_logs? |
|
51 |
+ event_created_within?(interpolated['expected_receive_period_in_days']) && !recent_error_logs? |
|
52 | 52 |
end |
53 | 53 |
|
54 | 54 |
def validate_options |
@@ -58,7 +58,7 @@ module Agents |
||
58 | 58 |
end |
59 | 59 |
|
60 | 60 |
def payload_for(params) |
61 |
- Utils.value_at(params, options['payload_path']) || {} |
|
61 |
+ Utils.value_at(params, interpolated['payload_path']) || {} |
|
62 | 62 |
end |
63 | 63 |
end |
64 | 64 |
end |
@@ -55,11 +55,11 @@ module Agents |
||
55 | 55 |
MD |
56 | 56 |
|
57 | 57 |
event_description do |
58 |
- "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print options['extract']}" |
|
58 |
+ "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print interpolated['extract']}" |
|
59 | 59 |
end |
60 | 60 |
|
61 | 61 |
def working? |
62 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
62 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
63 | 63 |
end |
64 | 64 |
|
65 | 65 |
def default_options |
@@ -125,7 +125,7 @@ module Agents |
||
125 | 125 |
end |
126 | 126 |
|
127 | 127 |
def check |
128 |
- check_url options['url'] |
|
128 |
+ check_url interpolated['url'] |
|
129 | 129 |
end |
130 | 130 |
|
131 | 131 |
def check_url(in_url) |
@@ -136,7 +136,7 @@ module Agents |
||
136 | 136 |
response = faraday.get(url) |
137 | 137 |
if response.success? |
138 | 138 |
body = response.body |
139 |
- if (encoding = options['force_encoding']).present? |
|
139 |
+ if (encoding = interpolated['force_encoding']).present? |
|
140 | 140 |
body = body.encode(Encoding::UTF_8, encoding) |
141 | 141 |
end |
142 | 142 |
doc = parse(body) |
@@ -148,7 +148,7 @@ module Agents |
||
148 | 148 |
end |
149 | 149 |
else |
150 | 150 |
output = {} |
151 |
- options['extract'].each do |name, extraction_details| |
|
151 |
+ interpolated['extract'].each do |name, extraction_details| |
|
152 | 152 |
if extraction_type == "json" |
153 | 153 |
result = Utils.values_at(doc, extraction_details['path']) |
154 | 154 |
log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
@@ -181,17 +181,17 @@ module Agents |
||
181 | 181 |
output[name] = result |
182 | 182 |
end |
183 | 183 |
|
184 |
- num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq |
|
184 |
+ num_unique_lengths = interpolated['extract'].keys.map { |name| output[name].length }.uniq |
|
185 | 185 |
|
186 | 186 |
if num_unique_lengths.length != 1 |
187 |
- error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}" |
|
187 |
+ error "Got an uneven number of matches for #{interpolated['name']}: #{interpolated['extract'].inspect}" |
|
188 | 188 |
return |
189 | 189 |
end |
190 | 190 |
|
191 | 191 |
old_events = previous_payloads num_unique_lengths.first |
192 | 192 |
num_unique_lengths.first.times do |index| |
193 | 193 |
result = {} |
194 |
- options['extract'].keys.each do |name| |
|
194 |
+ interpolated['extract'].keys.each do |name| |
|
195 | 195 |
result[name] = output[name][index] |
196 | 196 |
if name.to_s == 'url' |
197 | 197 |
result[name] = (response.env[:url] + result[name]).to_s |
@@ -223,11 +223,11 @@ module Agents |
||
223 | 223 |
# If mode is set to 'on_change', this method may return false and update an existing |
224 | 224 |
# event to expire further in the future. |
225 | 225 |
def store_payload!(old_events, result) |
226 |
- if !options['mode'].present? |
|
226 |
+ if !interpolated['mode'].present? |
|
227 | 227 |
return true |
228 |
- elsif options['mode'].to_s == "all" |
|
228 |
+ elsif interpolated['mode'].to_s == "all" |
|
229 | 229 |
return true |
230 |
- elsif options['mode'].to_s == "on_change" |
|
230 |
+ elsif interpolated['mode'].to_s == "on_change" |
|
231 | 231 |
result_json = result.to_json |
232 | 232 |
old_events.each do |old_event| |
233 | 233 |
if old_event.payload.to_json == result_json |
@@ -238,12 +238,12 @@ module Agents |
||
238 | 238 |
end |
239 | 239 |
return true |
240 | 240 |
end |
241 |
- raise "Illegal options[mode]: " + options['mode'].to_s |
|
241 |
+ raise "Illegal options[mode]: " + interpolated['mode'].to_s |
|
242 | 242 |
end |
243 | 243 |
|
244 | 244 |
def previous_payloads(num_events) |
245 |
- if options['uniqueness_look_back'].present? |
|
246 |
- look_back = options['uniqueness_look_back'].to_i |
|
245 |
+ if interpolated['uniqueness_look_back'].present? |
|
246 |
+ look_back = interpolated['uniqueness_look_back'].to_i |
|
247 | 247 |
else |
248 | 248 |
# Larger of UNIQUENESS_FACTOR * num_events and UNIQUENESS_LOOK_BACK |
249 | 249 |
look_back = UNIQUENESS_FACTOR * num_events |
@@ -251,18 +251,18 @@ module Agents |
||
251 | 251 |
look_back = UNIQUENESS_LOOK_BACK |
252 | 252 |
end |
253 | 253 |
end |
254 |
- events.order("id desc").limit(look_back) if options['mode'].present? && options['mode'].to_s == "on_change" |
|
254 |
+ events.order("id desc").limit(look_back) if interpolated['mode'].present? && interpolated['mode'].to_s == "on_change" |
|
255 | 255 |
end |
256 | 256 |
|
257 | 257 |
def extract_full_json? |
258 |
- !options['extract'].present? && extraction_type == "json" |
|
258 |
+ !interpolated['extract'].present? && extraction_type == "json" |
|
259 | 259 |
end |
260 | 260 |
|
261 | 261 |
def extraction_type |
262 |
- (options['type'] || begin |
|
263 |
- if options['url'] =~ /\.(rss|xml)$/i |
|
262 |
+ (interpolated['type'] || begin |
|
263 |
+ if interpolated['url'] =~ /\.(rss|xml)$/i |
|
264 | 264 |
"xml" |
265 |
- elsif options['url'] =~ /\.json$/i |
|
265 |
+ elsif interpolated['url'] =~ /\.json$/i |
|
266 | 266 |
"json" |
267 | 267 |
else |
268 | 268 |
"html" |
@@ -295,7 +295,7 @@ module Agents |
||
295 | 295 |
@faraday ||= Faraday.new { |builder| |
296 | 296 |
builder.headers = headers if headers.length > 0 |
297 | 297 |
|
298 |
- if (user_agent = options['user_agent']).present? |
|
298 |
+ if (user_agent = interpolated['user_agent']).present? |
|
299 | 299 |
builder.headers[:user_agent] = user_agent |
300 | 300 |
end |
301 | 301 |
|
@@ -318,7 +318,7 @@ module Agents |
||
318 | 318 |
end |
319 | 319 |
|
320 | 320 |
def basic_auth_credentials |
321 |
- case value = options['basic_auth'] |
|
321 |
+ case value = interpolated['basic_auth'] |
|
322 | 322 |
when nil, '' |
323 | 323 |
return nil |
324 | 324 |
when Array |
@@ -330,7 +330,7 @@ module Agents |
||
330 | 330 |
end |
331 | 331 |
|
332 | 332 |
def headers |
333 |
- options['headers'].presence || {} |
|
333 |
+ interpolated['headers'].presence || {} |
|
334 | 334 |
end |
335 | 335 |
end |
336 | 336 |
end |
@@ -21,13 +21,13 @@ module Agents |
||
21 | 21 |
|
22 | 22 |
def validate_options |
23 | 23 |
unless options['uid'].present? && |
24 |
- options['expected_update_period_in_days'].present? |
|
24 |
+ options['expected_update_period_in_days'].present? |
|
25 | 25 |
errors.add(:base, "expected_update_period_in_days and uid are required") |
26 | 26 |
end |
27 | 27 |
end |
28 | 28 |
|
29 | 29 |
def working? |
30 |
- event_created_within?(options['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
30 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? |
|
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def default_options |
@@ -47,7 +47,7 @@ module Agents |
||
47 | 47 |
incoming_events = incoming_events.first(20) |
48 | 48 |
end |
49 | 49 |
incoming_events.each do |event| |
50 |
- tweet_text = Utils.value_at(event.payload, options['message_path']) |
|
50 |
+ tweet_text = Utils.value_at(event.payload, interpolated(event.payload)['message_path']) |
|
51 | 51 |
if event.agent.type == "Agents::TwitterUserAgent" |
52 | 52 |
tweet_text = unwrap_tco_urls(tweet_text, event.payload) |
53 | 53 |
end |
@@ -71,13 +71,13 @@ module Agents |
||
71 | 71 |
|
72 | 72 |
def validate_options |
73 | 73 |
unless options['uid'].present? && |
74 |
- options['expected_update_period_in_days'].present? |
|
74 |
+ options['expected_update_period_in_days'].present? |
|
75 | 75 |
errors.add(:base, "expected_update_period_in_days and uid are required") |
76 | 76 |
end |
77 | 77 |
end |
78 | 78 |
|
79 | 79 |
def working? |
80 |
- event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
80 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
81 | 81 |
end |
82 | 82 |
|
83 | 83 |
def default_options |
@@ -92,7 +92,7 @@ module Agents |
||
92 | 92 |
|
93 | 93 |
def check |
94 | 94 |
since_id = memory['since_id'] || nil |
95 |
- opts = {:uid => options['uid'].to_i} |
|
95 |
+ opts = {:uid => interpolated['uid'].to_i} |
|
96 | 96 |
opts.merge! :since_id => since_id unless since_id.nil? |
97 | 97 |
|
98 | 98 |
# http://open.weibo.com/wiki/2/statuses/user_timeline/en |
@@ -0,0 +1,19 @@ |
||
1 |
+class Scenario < ActiveRecord::Base |
|
2 |
+ include HasGuid |
|
3 |
+ |
|
4 |
+ attr_accessible :name, :agent_ids, :description, :public, :source_url |
|
5 |
+ |
|
6 |
+ belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios |
|
7 |
+ has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario |
|
8 |
+ has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios |
|
9 |
+ |
|
10 |
+ validates_presence_of :name, :user |
|
11 |
+ |
|
12 |
+ validate :agents_are_owned |
|
13 |
+ |
|
14 |
+ protected |
|
15 |
+ |
|
16 |
+ def agents_are_owned |
|
17 |
+ errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } |
|
18 |
+ end |
|
19 |
+end |
@@ -0,0 +1,256 @@ |
||
1 |
+require 'ostruct' |
|
2 |
+ |
|
3 |
+# This is a helper class for managing Scenario imports, used by the ScenarioImportsController. This class behaves much |
|
4 |
+# like a normal ActiveRecord object, with validations and callbacks. However, it is never persisted to the database. |
|
5 |
+class ScenarioImport |
|
6 |
+ include ActiveModel::Model |
|
7 |
+ include ActiveModel::Callbacks |
|
8 |
+ include ActiveModel::Validations::Callbacks |
|
9 |
+ |
|
10 |
+ DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent] |
|
11 |
+ URL_REGEX = /\Ahttps?:\/\//i |
|
12 |
+ |
|
13 |
+ attr_accessor :file, :url, :data, :do_import, :merges |
|
14 |
+ |
|
15 |
+ attr_reader :user |
|
16 |
+ |
|
17 |
+ before_validation :parse_file |
|
18 |
+ before_validation :fetch_url |
|
19 |
+ |
|
20 |
+ validate :validate_presence_of_file_url_or_data |
|
21 |
+ validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid" |
|
22 |
+ validate :validate_data |
|
23 |
+ validate :generate_diff |
|
24 |
+ |
|
25 |
+ def step_one? |
|
26 |
+ data.blank? |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def step_two? |
|
30 |
+ data.present? |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def set_user(user) |
|
34 |
+ @user = user |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def existing_scenario |
|
38 |
+ @existing_scenario ||= user.scenarios.find_by(:guid => parsed_data["guid"]) |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def dangerous? |
|
42 |
+ (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) } |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def parsed_data |
|
46 |
+ @parsed_data ||= (data && JSON.parse(data) rescue {}) || {} |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def agent_diffs |
|
50 |
+ @agent_diffs || generate_diff |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ def should_import? |
|
54 |
+ do_import == "1" |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def import(options = {}) |
|
58 |
+ success = true |
|
59 |
+ guid = parsed_data['guid'] |
|
60 |
+ description = parsed_data['description'] |
|
61 |
+ name = parsed_data['name'] |
|
62 |
+ links = parsed_data['links'] |
|
63 |
+ source_url = parsed_data['source_url'].presence || nil |
|
64 |
+ @scenario = user.scenarios.where(:guid => guid).first_or_initialize |
|
65 |
+ @scenario.update_attributes!(:name => name, :description => description, |
|
66 |
+ :source_url => source_url, :public => false) |
|
67 |
+ |
|
68 |
+ unless options[:skip_agents] |
|
69 |
+ created_agents = agent_diffs.map do |agent_diff| |
|
70 |
+ agent = agent_diff.agent || Agent.build_for_type("Agents::" + agent_diff.type.incoming, user) |
|
71 |
+ agent.guid = agent_diff.guid.incoming |
|
72 |
+ agent.attributes = { :name => agent_diff.name.updated, |
|
73 |
+ :disabled => agent_diff.disabled.updated, # == "true" |
|
74 |
+ :options => agent_diff.options.updated, |
|
75 |
+ :scenario_ids => [@scenario.id] } |
|
76 |
+ agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present? |
|
77 |
+ agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present? |
|
78 |
+ agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true" |
|
79 |
+ unless agent.save |
|
80 |
+ success = false |
|
81 |
+ errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}") |
|
82 |
+ end |
|
83 |
+ agent |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ links.each do |link| |
|
87 |
+ receiver = created_agents[link['receiver']] |
|
88 |
+ source = created_agents[link['source']] |
|
89 |
+ receiver.sources << source unless receiver.sources.include?(source) |
|
90 |
+ end |
|
91 |
+ end |
|
92 |
+ |
|
93 |
+ success |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ def scenario |
|
97 |
+ @scenario || @existing_scenario |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ def will_request_local?(url_root) |
|
101 |
+ data.blank? && file.blank? && url.present? && url.starts_with?(url_root) |
|
102 |
+ end |
|
103 |
+ |
|
104 |
+ protected |
|
105 |
+ |
|
106 |
+ def parse_file |
|
107 |
+ if data.blank? && file.present? |
|
108 |
+ self.data = file.read |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+ |
|
112 |
+ def fetch_url |
|
113 |
+ if data.blank? && url.present? && url =~ URL_REGEX |
|
114 |
+ self.data = Faraday.get(url).body |
|
115 |
+ end |
|
116 |
+ end |
|
117 |
+ |
|
118 |
+ def validate_data |
|
119 |
+ if data.present? |
|
120 |
+ @parsed_data = JSON.parse(data) rescue {} |
|
121 |
+ if (%w[name guid agents] - @parsed_data.keys).length > 0 |
|
122 |
+ errors.add(:base, "The provided data does not appear to be a valid Scenario.") |
|
123 |
+ self.data = nil |
|
124 |
+ end |
|
125 |
+ else |
|
126 |
+ @parsed_data = nil |
|
127 |
+ end |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ def validate_presence_of_file_url_or_data |
|
131 |
+ unless file.present? || url.present? || data.present? |
|
132 |
+ errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
133 |
+ end |
|
134 |
+ end |
|
135 |
+ |
|
136 |
+ def generate_diff |
|
137 |
+ @agent_diffs = (parsed_data['agents'] || []).map.with_index do |agent_data, index| |
|
138 |
+ # AgentDiff is defined at the end of this file. |
|
139 |
+ agent_diff = AgentDiff.new(agent_data) |
|
140 |
+ if existing_scenario |
|
141 |
+ # If this Agent exists already, update the AgentDiff with the local version's information. |
|
142 |
+ agent_diff.diff_with! existing_scenario.agents.find_by(:guid => agent_data['guid']) |
|
143 |
+ |
|
144 |
+ begin |
|
145 |
+ # Update the AgentDiff with any hand-merged changes coming from the UI. This only happens when this |
|
146 |
+ # Agent already exists locally and has conflicting changes. |
|
147 |
+ agent_diff.update_from! merges[index.to_s] if merges |
|
148 |
+ rescue JSON::ParserError |
|
149 |
+ errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.") |
|
150 |
+ end |
|
151 |
+ end |
|
152 |
+ agent_diff |
|
153 |
+ end |
|
154 |
+ end |
|
155 |
+ |
|
156 |
+ # AgentDiff is a helper object that encapsulates an incoming Agent. All fields will be returned as an array |
|
157 |
+ # of either one or two values. The first value is the incoming value, the second is the existing value, if |
|
158 |
+ # it differs from the incoming value. |
|
159 |
+ class AgentDiff < OpenStruct |
|
160 |
+ class FieldDiff |
|
161 |
+ attr_accessor :incoming, :current, :updated |
|
162 |
+ |
|
163 |
+ def initialize(incoming) |
|
164 |
+ @incoming = incoming |
|
165 |
+ @updated = incoming |
|
166 |
+ end |
|
167 |
+ |
|
168 |
+ def set_current(current) |
|
169 |
+ @current = current |
|
170 |
+ @requires_merge = (incoming != current) |
|
171 |
+ end |
|
172 |
+ |
|
173 |
+ def requires_merge? |
|
174 |
+ @requires_merge |
|
175 |
+ end |
|
176 |
+ end |
|
177 |
+ |
|
178 |
+ def initialize(agent_data) |
|
179 |
+ super() |
|
180 |
+ @requires_merge = false |
|
181 |
+ self.agent = nil |
|
182 |
+ store! agent_data |
|
183 |
+ end |
|
184 |
+ |
|
185 |
+ BASE_FIELDS = %w[name schedule keep_events_for propagate_immediately disabled guid] |
|
186 |
+ |
|
187 |
+ def agent_exists? |
|
188 |
+ !!agent |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ def requires_merge? |
|
192 |
+ @requires_merge |
|
193 |
+ end |
|
194 |
+ |
|
195 |
+ def store!(agent_data) |
|
196 |
+ self.type = FieldDiff.new(agent_data["type"].split("::").pop) |
|
197 |
+ self.options = FieldDiff.new(agent_data['options'] || {}) |
|
198 |
+ BASE_FIELDS.each do |option| |
|
199 |
+ self[option] = FieldDiff.new(agent_data[option]) if agent_data.has_key?(option) |
|
200 |
+ end |
|
201 |
+ end |
|
202 |
+ |
|
203 |
+ def diff_with!(agent) |
|
204 |
+ return unless agent.present? |
|
205 |
+ |
|
206 |
+ self.agent = agent |
|
207 |
+ |
|
208 |
+ type.set_current(agent.short_type) |
|
209 |
+ options.set_current(agent.options || {}) |
|
210 |
+ |
|
211 |
+ @requires_merge ||= type.requires_merge? |
|
212 |
+ @requires_merge ||= options.requires_merge? |
|
213 |
+ |
|
214 |
+ BASE_FIELDS.each do |field| |
|
215 |
+ next unless self[field].present? |
|
216 |
+ self[field].set_current(agent.send(field)) |
|
217 |
+ |
|
218 |
+ @requires_merge ||= self[field].requires_merge? |
|
219 |
+ end |
|
220 |
+ end |
|
221 |
+ |
|
222 |
+ def update_from!(merges) |
|
223 |
+ each_field do |field, value, selection_options| |
|
224 |
+ value.updated = merges[field] |
|
225 |
+ end |
|
226 |
+ |
|
227 |
+ if options.requires_merge? |
|
228 |
+ options.updated = JSON.parse(merges['options']) |
|
229 |
+ end |
|
230 |
+ end |
|
231 |
+ |
|
232 |
+ def each_field |
|
233 |
+ boolean = [["True", "true"], ["False", "false"]] |
|
234 |
+ yield 'name', name if name.requires_merge? |
|
235 |
+ yield 'schedule', schedule, Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] } if self['schedule'].present? && schedule.requires_merge? |
|
236 |
+ yield 'keep_events_for', keep_events_for, Agent::EVENT_RETENTION_SCHEDULES if self['keep_events_for'].present? && keep_events_for.requires_merge? |
|
237 |
+ yield 'propagate_immediately', propagate_immediately, boolean if self['propagate_immediately'].present? && propagate_immediately.requires_merge? |
|
238 |
+ yield 'disabled', disabled, boolean if disabled.requires_merge? |
|
239 |
+ end |
|
240 |
+ |
|
241 |
+ # Unfortunately Ruby 1.9's OpenStruct doesn't expose [] and []=. |
|
242 |
+ unless instance_methods.include?(:[]=) |
|
243 |
+ def [](key) |
|
244 |
+ self.send(sanitize key) |
|
245 |
+ end |
|
246 |
+ |
|
247 |
+ def []=(key, val) |
|
248 |
+ self.send("#{sanitize key}=", val) |
|
249 |
+ end |
|
250 |
+ |
|
251 |
+ def sanitize(key) |
|
252 |
+ key.gsub(/[^a-zA-Z0-9_-]/, '') |
|
253 |
+ end |
|
254 |
+ end |
|
255 |
+ end |
|
256 |
+end |
@@ -0,0 +1,4 @@ |
||
1 |
+class ScenarioMembership < ActiveRecord::Base |
|
2 |
+ belongs_to :agent, :inverse_of => :scenario_memberships |
|
3 |
+ belongs_to :scenario, :inverse_of => :scenario_memberships |
|
4 |
+end |
@@ -26,11 +26,11 @@ class User < ActiveRecord::Base |
||
26 | 26 |
has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user |
27 | 27 |
has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user |
28 | 28 |
has_many :logs, :through => :agents, :class_name => "AgentLog" |
29 |
+ has_many :scenarios, :inverse_of => :user, :dependent => :destroy |
|
29 | 30 |
has_many :services, -> { order("services.name")}, :dependent => :destroy |
30 |
- |
|
31 | 31 |
|
32 | 32 |
def available_services |
33 |
- Service.where("user_id = ? or global = true", self.id).order("services.name desc") |
|
33 |
+ Service.where("user_id = ? or global = true", self.id).order("services.name desc") |
|
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
# Allow users to login via either email or username. |
@@ -27,15 +27,25 @@ |
||
27 | 27 |
<% end %> |
28 | 28 |
</li> |
29 | 29 |
|
30 |
+ <% if agent.scenarios.length > 0 %> |
|
31 |
+ <li class="divider"></li> |
|
32 |
+ |
|
33 |
+ <% agent.scenarios.each do |scenario| %> |
|
34 |
+ <li> |
|
35 |
+ <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from <span class='scenario label label-info'>#{h scenario.name}</span>".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
36 |
+ </li> |
|
37 |
+ <% end %> |
|
38 |
+ <% end %> |
|
39 |
+ |
|
30 | 40 |
<li class="divider"></li> |
31 | 41 |
|
32 | 42 |
<% if agent.can_create_events? && agent.events.count > 0 %> |
33 | 43 |
<li> |
34 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent), method: :delete, data: {confirm: 'Are you sure you want to delete ALL events for this Agent?'}, :tabindex => "-1" %> |
|
44 |
+ <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent, :return => returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, :tabindex => "-1" %> |
|
35 | 45 |
</li> |
36 | 46 |
<% end %> |
37 | 47 |
|
38 | 48 |
<li> |
39 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent), method: :delete, data: { confirm: 'Are you sure?' }, :tabindex => "-1" %> |
|
49 |
+ <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent, :return => returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, :tabindex => "-1" %> |
|
40 | 50 |
</li> |
41 | 51 |
</ul> |
@@ -50,6 +50,7 @@ |
||
50 | 50 |
<div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
51 | 51 |
<div class="form-group"> |
52 | 52 |
<%= f.label :keep_events_for, "Keep events" %> |
53 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
53 | 54 |
<%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %> |
54 | 55 |
</div> |
55 | 56 |
</div> |
@@ -68,13 +69,24 @@ |
||
68 | 69 |
<% end %> |
69 | 70 |
</div> |
70 | 71 |
</div> |
72 |
+ |
|
73 |
+ <% if current_user.scenario_count > 0 %> |
|
74 |
+ <div class="form-group"> |
|
75 |
+ <%= f.label :scenarios %> |
|
76 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
77 |
+ <%= f.select(:scenario_ids, |
|
78 |
+ options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
79 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
80 |
+ </div> |
|
81 |
+ <% end %> |
|
82 |
+ |
|
71 | 83 |
</div> |
72 | 84 |
|
73 | 85 |
<!-- Form controls full width --> |
74 | 86 |
<div class="col-md-12"> |
75 | 87 |
<div class="form-group"> |
76 | 88 |
<%= f.label :options %> |
77 |
- <textarea rows="10" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
|
89 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
|
78 | 90 |
<%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
79 | 91 |
</textarea> |
80 | 92 |
</div> |
@@ -101,7 +113,7 @@ |
||
101 | 113 |
|
102 | 114 |
<div class='row'> |
103 | 115 |
<div class="col-md-12"> |
104 |
- <%= f.submit :class => "btn btn-primary" %> |
|
116 |
+ <%= f.submit "Save", :class => "btn btn-primary" %> |
|
105 | 117 |
</div> |
106 | 118 |
</div> |
107 | 119 |
|
@@ -0,0 +1,75 @@ |
||
1 |
+<div class='table-responsive'> |
|
2 |
+ <table class='table table-striped'> |
|
3 |
+ <tr> |
|
4 |
+ <th>Name</th> |
|
5 |
+ <th>Schedule</th> |
|
6 |
+ <th>Last Check</th> |
|
7 |
+ <th>Last Event Out</th> |
|
8 |
+ <th>Last Event In</th> |
|
9 |
+ <th>Events Created</th> |
|
10 |
+ <th>Working?</th> |
|
11 |
+ <th></th> |
|
12 |
+ </tr> |
|
13 |
+ |
|
14 |
+ <% @agents.each do |agent| %> |
|
15 |
+ <tr> |
|
16 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
17 |
+ <%= link_to agent.name, agent_path(agent) %> |
|
18 |
+ <br/> |
|
19 |
+ <span class='text-muted'><%= agent.short_type.titleize %></span> |
|
20 |
+ <% if agent.scenarios.present? %> |
|
21 |
+ <span> |
|
22 |
+ <%= scenario_links(agent) %> |
|
23 |
+ </span> |
|
24 |
+ <% end %> |
|
25 |
+ </td> |
|
26 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
27 |
+ <% if agent.can_be_scheduled? %> |
|
28 |
+ <%= agent.schedule.to_s.humanize.titleize %> |
|
29 |
+ <% else %> |
|
30 |
+ <span class='not-applicable'></span> |
|
31 |
+ <% end %> |
|
32 |
+ </td> |
|
33 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
34 |
+ <% if agent.can_be_scheduled? %> |
|
35 |
+ <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
36 |
+ <% else %> |
|
37 |
+ <span class='not-applicable'></span> |
|
38 |
+ <% end %> |
|
39 |
+ </td> |
|
40 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
41 |
+ <% if agent.can_create_events? %> |
|
42 |
+ <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
|
43 |
+ <% else %> |
|
44 |
+ <span class='not-applicable'></span> |
|
45 |
+ <% end %> |
|
46 |
+ </td> |
|
47 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
48 |
+ <% if agent.can_receive_events? %> |
|
49 |
+ <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
50 |
+ <% else %> |
|
51 |
+ <span class='not-applicable'></span> |
|
52 |
+ <% end %> |
|
53 |
+ </td> |
|
54 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
55 |
+ <% if agent.can_create_events? %> |
|
56 |
+ <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
57 |
+ <% else %> |
|
58 |
+ <span class='not-applicable'></span> |
|
59 |
+ <% end %> |
|
60 |
+ </td> |
|
61 |
+ <td><%= working(agent) %></td> |
|
62 |
+ <td> |
|
63 |
+ <div class="btn-group"> |
|
64 |
+ <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"> |
|
65 |
+ <span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span> |
|
66 |
+ </button> |
|
67 |
+ <%= render 'agents/action_menu', :agent => agent, :returnTo => (defined?(returnTo) && returnTo) || "index" %> |
|
68 |
+ </div> |
|
69 |
+ </td> |
|
70 |
+ </tr> |
|
71 |
+ <% end %> |
|
72 |
+ </table> |
|
73 |
+</div> |
|
74 |
+ |
|
75 |
+<%= paginate @agents, :theme => 'twitter-bootstrap-3' %> |
@@ -14,7 +14,7 @@ |
||
14 | 14 |
|
15 | 15 |
<script> |
16 | 16 |
$(function () { |
17 |
- var payloadJsonEditor = window.setupJsonEditor($(".payload-editor")); |
|
17 |
+ var payloadJsonEditor = window.setupJsonEditor($(".payload-editor"))[0]; |
|
18 | 18 |
$("#create-event-form").submit(function (e) { |
19 | 19 |
e.preventDefault(); |
20 | 20 |
var $form = $("#create-event-form"); |
@@ -5,7 +5,7 @@ |
||
5 | 5 |
<h2>Agent Event Flow</h2> |
6 | 6 |
</div> |
7 | 7 |
<div class="btn-group"> |
8 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
8 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %> |
|
9 | 9 |
</div> |
10 | 10 |
|
11 | 11 |
<div class='digraph'> |
@@ -5,76 +5,7 @@ |
||
5 | 5 |
<h2>Your Agents</h2> |
6 | 6 |
</div> |
7 | 7 |
|
8 |
- <div class='table-responsive'> |
|
9 |
- <table class='table table-striped'> |
|
10 |
- <tr> |
|
11 |
- <th>Name</th> |
|
12 |
- <th>Schedule</th> |
|
13 |
- <th>Last Check</th> |
|
14 |
- <th>Last Event Out</th> |
|
15 |
- <th>Last Event In</th> |
|
16 |
- <th>Events Created</th> |
|
17 |
- <th>Working?</th> |
|
18 |
- <th></th> |
|
19 |
- </tr> |
|
20 |
- |
|
21 |
- <% @agents.each do |agent| %> |
|
22 |
- <tr> |
|
23 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
24 |
- <%= link_to agent.name, agent_path(agent) %> |
|
25 |
- <br/> |
|
26 |
- <span class='text-muted'><%= agent.short_type.titleize %></span> |
|
27 |
- </td> |
|
28 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
29 |
- <% if agent.can_be_scheduled? %> |
|
30 |
- <%= agent.schedule.to_s.humanize.titleize %> |
|
31 |
- <% else %> |
|
32 |
- <span class='not-applicable'></span> |
|
33 |
- <% end %> |
|
34 |
- </td> |
|
35 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
36 |
- <% if agent.can_be_scheduled? %> |
|
37 |
- <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
38 |
- <% else %> |
|
39 |
- <span class='not-applicable'></span> |
|
40 |
- <% end %> |
|
41 |
- </td> |
|
42 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
43 |
- <% if agent.can_create_events? %> |
|
44 |
- <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
|
45 |
- <% else %> |
|
46 |
- <span class='not-applicable'></span> |
|
47 |
- <% end %> |
|
48 |
- </td> |
|
49 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
50 |
- <% if agent.can_receive_events? %> |
|
51 |
- <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
52 |
- <% else %> |
|
53 |
- <span class='not-applicable'></span> |
|
54 |
- <% end %> |
|
55 |
- </td> |
|
56 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
57 |
- <% if agent.can_create_events? %> |
|
58 |
- <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
59 |
- <% else %> |
|
60 |
- <span class='not-applicable'></span> |
|
61 |
- <% end %> |
|
62 |
- </td> |
|
63 |
- <td><%= working(agent) %></td> |
|
64 |
- <td> |
|
65 |
- <div class="btn-group"> |
|
66 |
- <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"> |
|
67 |
- <span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span> |
|
68 |
- </button> |
|
69 |
- <%= render 'action_menu', :agent => agent, :returnTo => "index" %> |
|
70 |
- </div> |
|
71 |
- </td> |
|
72 |
- </tr> |
|
73 |
- <% end %> |
|
74 |
- </table> |
|
75 |
- </div> |
|
76 |
- |
|
77 |
- <%= paginate @agents, :theme => 'twitter-bootstrap-3' %> |
|
8 |
+ <%= render 'agents/table' %> |
|
78 | 9 |
|
79 | 10 |
<br/> |
80 | 11 |
|
@@ -22,7 +22,7 @@ |
||
22 | 22 |
|
23 | 23 |
<li class="dropdown"> |
24 | 24 |
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span></a> |
25 |
- <%= render 'action_menu', :agent => @agent, :returnTo => "show" %> |
|
25 |
+ <%= render 'agents/action_menu', :agent => @agent, :returnTo => "show" %> |
|
26 | 26 |
</li> |
27 | 27 |
</ul> |
28 | 28 |
</div> |
@@ -20,13 +20,13 @@ |
||
20 | 20 |
<% next unless event.agent %> |
21 | 21 |
<tr> |
22 | 22 |
<td><%= link_to event.agent.name, agent_path(event.agent) %></td> |
23 |
- <td><%= time_ago_in_words event.created_at %> ago</td> |
|
23 |
+ <td title='<%= event.created_at %>'><%= time_ago_in_words event.created_at %> ago</td> |
|
24 | 24 |
<td class='payload'><%= truncate event.payload.to_json, :length => 90, :omission => "" %></td> |
25 | 25 |
<td> |
26 | 26 |
<div class="btn-group btn-group-xs"> |
27 | 27 |
<%= link_to 'Show', event_path(event), class: "btn btn-default" %> |
28 | 28 |
<%= link_to 'Re-emit', reemit_event_path(event), method: :post, data: { confirm: 'Are you sure you want to duplicate this event and emit the new one now?' }, class: "btn btn-default" %> |
29 |
- <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %> |
|
29 |
+ <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default" %> |
|
30 | 30 |
</div> |
31 | 31 |
</td> |
32 | 32 |
</tr> |
@@ -13,6 +13,7 @@ |
||
13 | 13 |
<% if user_signed_in? %> |
14 | 14 |
<ul class='nav navbar-nav'> |
15 | 15 |
<%= nav_link "Agents", agents_path %> |
16 |
+ <%= nav_link "Scenarios", scenarios_path %> |
|
16 | 17 |
<%= nav_link "Events", events_path %> |
17 | 18 |
<%= nav_link "Credentials", user_credentials_path %> |
18 | 19 |
<%= nav_link "Services", services_path %> |
@@ -31,18 +31,22 @@ |
||
31 | 31 |
|
32 | 32 |
<script> |
33 | 33 |
var agentPaths = {}; |
34 |
- <% if current_user -%> |
|
35 |
- var myAgents = <%= Utils.jsonify(current_user.agents.select([:name, :id, :schedule]).inject({}) {|m, a| m[a.name] = agent_path(a); m }) %>; |
|
34 |
+ var agentNames = []; |
|
35 |
+ <% if current_user.present? -%> |
|
36 |
+ var myAgents = <%= Utils.jsonify(current_user.agents.pluck(:name, :id).inject({}) {|m, a| m[a.first] = agent_path(a.last); m }) %>; |
|
37 |
+ var myScenarios = <%= Utils.jsonify(current_user.scenarios.pluck(:name, :id).inject({}) {|m, s| m[s.first + " Scenario"] = scenario_path(s.last); m }) %>; |
|
36 | 38 |
$.extend(agentPaths, myAgents); |
39 |
+ $.extend(agentPaths, myScenarios); |
|
40 |
+ agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
41 |
+ agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
42 |
+ agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
43 |
+ agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
44 |
+ agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; |
|
45 |
+ agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
46 |
+ |
|
47 |
+ |
|
48 |
+ $.each(agentPaths, function(name, v) { agentNames.push(name); }); |
|
37 | 49 |
<% end -%> |
38 |
- agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
39 |
- agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
40 |
- agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
41 |
- agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
42 |
- agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; |
|
43 |
- agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
44 |
- var agentNames = []; |
|
45 |
- $.each(agentPaths, function(name, v) { agentNames.push(name); }); |
|
46 | 50 |
</script> |
47 | 51 |
</body> |
48 | 52 |
</html> |
@@ -0,0 +1,31 @@ |
||
1 |
+<div class="row"> |
|
2 |
+ <div class="page-header"> |
|
3 |
+ <h2> |
|
4 |
+ Import a Public Scenario |
|
5 |
+ </h2> |
|
6 |
+ </div> |
|
7 |
+</div> |
|
8 |
+ |
|
9 |
+<div class='row'> |
|
10 |
+ <blockquote>You can import Scenarios, either from a <code>.json</code> file, or via a public |
|
11 |
+ Scenario URL. When you import a Scenario, Huginn will keep track of where it came from and |
|
12 |
+ later let you update it.</blockquote> |
|
13 |
+</div> |
|
14 |
+ |
|
15 |
+<div class='row'> |
|
16 |
+ <div class="col-md-4"> |
|
17 |
+ <div class="form-group"> |
|
18 |
+ <%= f.label :url, 'Option 1: Provide a Public Scenario URL' %> |
|
19 |
+ <%= f.text_field :url, :class => 'form-control', :placeholder => "Public Scenario URL" %> |
|
20 |
+ </div> |
|
21 |
+ |
|
22 |
+ <div class="form-group"> |
|
23 |
+ <%= f.label :file, 'Option 2: Upload a Scenario JSON File' %> |
|
24 |
+ <%= f.file_field :file, :class => 'form-control' %> |
|
25 |
+ </div> |
|
26 |
+ |
|
27 |
+ <div class='form-actions'> |
|
28 |
+ <%= f.submit "Start Import", :class => "btn btn-primary" %> |
|
29 |
+ </div> |
|
30 |
+ </div> |
|
31 |
+</div> |
@@ -0,0 +1,154 @@ |
||
1 |
+<div class="row"> |
|
2 |
+ <div class="col-md-12"> |
|
3 |
+ <% if @scenario_import.dangerous? %> |
|
4 |
+ <div class="alert alert-danger"> |
|
5 |
+ <span class='glyphicon glyphicon-warning-sign'></span> |
|
6 |
+ This Scenario contains one or more potentially dangerous Agents. |
|
7 |
+ These may be able to run local commands or execute code. |
|
8 |
+ Please be sure that you understand the Agent configurations before importing! |
|
9 |
+ </div> |
|
10 |
+ <% end %> |
|
11 |
+ |
|
12 |
+ <% if @scenario_import.existing_scenario.present? %> |
|
13 |
+ <div class="alert alert-warning"> |
|
14 |
+ <span class='glyphicon glyphicon-warning-sign'></span> |
|
15 |
+ This Scenario already exists in your system. The import will update your existing |
|
16 |
+ <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario's title |
|
17 |
+ and |
|
18 |
+ description. Below you can customize how the individual agents get updated. |
|
19 |
+ </div> |
|
20 |
+ <% end %> |
|
21 |
+ |
|
22 |
+ <div class="page-header"> |
|
23 |
+ <h2> |
|
24 |
+ <%= @scenario_import.parsed_data["name"] %> |
|
25 |
+ <span class='text-muted'> |
|
26 |
+ (<%= pluralize @scenario_import.parsed_data["agents"].length, "Agent" %>; |
|
27 |
+ exported <%= time_ago_in_words Time.parse(@scenario_import.parsed_data["exported_at"]) %> ago) |
|
28 |
+ </span> |
|
29 |
+ </h2> |
|
30 |
+ </div> |
|
31 |
+ |
|
32 |
+ <% if @scenario_import.parsed_data["description"].present? %> |
|
33 |
+ <blockquote><%= @scenario_import.parsed_data["description"] %></blockquote> |
|
34 |
+ <% end %> |
|
35 |
+ |
|
36 |
+ </div> |
|
37 |
+</div> |
|
38 |
+ |
|
39 |
+<div class='agent-import-list'> |
|
40 |
+ <% @scenario_import.agent_diffs.each.with_index do |agent_diff, index| %> |
|
41 |
+ <div class='agent-import' data-index='<%= index %>'> |
|
42 |
+ |
|
43 |
+ <div class='row'> |
|
44 |
+ <div class='col-md-12'> |
|
45 |
+ <h3> |
|
46 |
+ <a href='#' data-toggle="modal" data-target="#agent_options_<%= index %>"><%= agent_diff.name.incoming %></a> |
|
47 |
+ <span class='text-muted'> |
|
48 |
+ (<%= agent_diff.type.incoming %><% " -- WARNING: this Agent's type has been changed. This import will likely fail!" if agent_diff.type.requires_merge? %>) |
|
49 |
+ </span> |
|
50 |
+ </h3> |
|
51 |
+ |
|
52 |
+ <% if agent_diff.agent_exists? %> |
|
53 |
+ <div class="instructions"> |
|
54 |
+ This Agent exists in your Huginn system. |
|
55 |
+ |
|
56 |
+ <% if agent_diff.requires_merge? %> |
|
57 |
+ Here are the differences between the incoming version and the one you have now. For each field, please |
|
58 |
+ select which value you'd like to keep. |
|
59 |
+ <% else %> |
|
60 |
+ It's already up-to-date. |
|
61 |
+ <% end %> |
|
62 |
+ </div> |
|
63 |
+ <% end %> |
|
64 |
+ </div> |
|
65 |
+ </div> |
|
66 |
+ |
|
67 |
+ <div class="modal fade" id="agent_options_<%= index %>" tabindex="-1" role="dialog" aria-labelledby="modalLabel<%= index %>" aria-hidden="true"> |
|
68 |
+ <div class="modal-dialog modal-lg"> |
|
69 |
+ <div class="modal-content"> |
|
70 |
+ <div class="modal-header"> |
|
71 |
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
72 |
+ <h4 class="modal-title" id="modalLabel<%= index %>">Options for '<%= agent_diff.name.updated %>'</h4> |
|
73 |
+ </div> |
|
74 |
+ <div class="modal-body"> |
|
75 |
+ <pre class='options'><%= Utils.pretty_jsonify agent_diff.options.incoming %></pre> |
|
76 |
+ </div> |
|
77 |
+ </div> |
|
78 |
+ </div> |
|
79 |
+ </div> |
|
80 |
+ |
|
81 |
+ <% agent_diff.each_field do |field, value, selection_options| %> |
|
82 |
+ <div class='row'> |
|
83 |
+ <div class='col-md-4'> |
|
84 |
+ <div class="form-group"> |
|
85 |
+ <%= label_tag "scenario_import[merges][#{index}][#{field}]", field.titleize %> |
|
86 |
+ <% if selection_options.present? %> |
|
87 |
+ <div> |
|
88 |
+ Your current Agent's value is: |
|
89 |
+ <span class='current'><%= selection_options.find { |s| s.last.to_s == value.current.to_s }.first %></span> |
|
90 |
+ </div> |
|
91 |
+ <%= select_tag "scenario_import[merges][#{index}][#{field}]", options_for_select(selection_options, value.updated), :class => 'form-control' %> |
|
92 |
+ <% else %> |
|
93 |
+ <div> |
|
94 |
+ Your current Agent's value is: <span class='current'><%= value.current.to_s %></span> |
|
95 |
+ </div> |
|
96 |
+ <%= text_field_tag "scenario_import[merges][#{index}][#{field}]", value.updated, :class => 'form-control' %> |
|
97 |
+ <% end %> |
|
98 |
+ </div> |
|
99 |
+ </div> |
|
100 |
+ </div> |
|
101 |
+ <% end %> |
|
102 |
+ |
|
103 |
+ <div class='row'> |
|
104 |
+ <% if agent_diff.options.requires_merge? %> |
|
105 |
+ <div class='col-md-12'> |
|
106 |
+ <label>Options</label> |
|
107 |
+ </div> |
|
108 |
+ |
|
109 |
+ <div class='col-md-6'> |
|
110 |
+ <textarea name="scenario_import[merges][<%= index %>][options]" rows='15' class="form-control live-json-editor"> |
|
111 |
+ <%= Utils.pretty_jsonify(agent_diff.options.updated) %> |
|
112 |
+ </textarea> |
|
113 |
+ </div> |
|
114 |
+ |
|
115 |
+ <div class='col-md-6'> |
|
116 |
+ <div> |
|
117 |
+ Your current options: |
|
118 |
+ </div> |
|
119 |
+ <pre class='options'><%= Utils.pretty_jsonify agent_diff.options.current %></pre> |
|
120 |
+ </div> |
|
121 |
+ <% end %> |
|
122 |
+ </div> |
|
123 |
+ </div> |
|
124 |
+ <% end %> |
|
125 |
+</div> |
|
126 |
+ |
|
127 |
+<div class='row'> |
|
128 |
+ <div class='col-md-12'> |
|
129 |
+ <div class="checkbox"> |
|
130 |
+ <%= f.label :do_import do %> |
|
131 |
+ <%= f.check_box :do_import %> I confirm that I want to import these Agents. |
|
132 |
+ <% end %> |
|
133 |
+ </div> |
|
134 |
+ |
|
135 |
+ <div class='form-actions'> |
|
136 |
+ <%= f.submit "Finish Import", :class => "btn btn-primary" %> |
|
137 |
+ </div> |
|
138 |
+ </div> |
|
139 |
+</div> |
|
140 |
+ |
|
141 |
+ |
|
142 |
+<script> |
|
143 |
+// $(function() { |
|
144 |
+// $('.agent-import-list .options-toggle').on('click', function (e) { |
|
145 |
+// e.preventDefault(); |
|
146 |
+// $(this).siblings('.options').slideToggle() |
|
147 |
+// if ($(this).text() == "Show Options") { |
|
148 |
+// $(this).text("Hide Options"); |
|
149 |
+// } else { |
|
150 |
+// $(this).text("Show Options"); |
|
151 |
+// } |
|
152 |
+// }); |
|
153 |
+// }); |
|
154 |
+</script> |
@@ -0,0 +1,32 @@ |
||
1 |
+<div class='container scenario-import'> |
|
2 |
+ <div class="row"> |
|
3 |
+ <div class="col-md-12"> |
|
4 |
+ <% if @scenario_import.errors.any? %> |
|
5 |
+ <div class="row well"> |
|
6 |
+ <h2><%= pluralize(@scenario_import.errors.count, "error") %> prohibited this Scenario from being imported:</h2> |
|
7 |
+ <% @scenario_import.errors.full_messages.each do |msg| %> |
|
8 |
+ <p class='text-warning'><%= msg %></p> |
|
9 |
+ <% end %> |
|
10 |
+ </div> |
|
11 |
+ <% end %> |
|
12 |
+ </div> |
|
13 |
+ </div> |
|
14 |
+ |
|
15 |
+ <%= form_for @scenario_import, :multipart => true do |f| %> |
|
16 |
+ <%= f.hidden_field :data %> |
|
17 |
+ |
|
18 |
+ <% if @scenario_import.step_one? %> |
|
19 |
+ <%= render 'step_one', :f => f %> |
|
20 |
+ <% elsif @scenario_import.step_two? %> |
|
21 |
+ <%= render 'step_two', :f => f %> |
|
22 |
+ <% end %> |
|
23 |
+ <% end %> |
|
24 |
+ |
|
25 |
+ <hr /> |
|
26 |
+ |
|
27 |
+ <div class="row"> |
|
28 |
+ <div class="col-md-12"> |
|
29 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+</div> |
@@ -0,0 +1,57 @@ |
||
1 |
+<%= form_for(@scenario, :method => @scenario.new_record? ? "POST" : "PUT") do |f| %> |
|
2 |
+ <% if @scenario.errors.any? %> |
|
3 |
+ <div class="row well"> |
|
4 |
+ <h2><%= pluralize(@scenario.errors.count, "error") %> prohibited this Scenario from being saved:</h2> |
|
5 |
+ <% @scenario.errors.full_messages.each do |msg| %> |
|
6 |
+ <p class='text-warning'><%= msg %></p> |
|
7 |
+ <% end %> |
|
8 |
+ </div> |
|
9 |
+ <% end %> |
|
10 |
+ |
|
11 |
+ <div class="row"> |
|
12 |
+ <div class="col-md-4"> |
|
13 |
+ <div class="form-group"> |
|
14 |
+ <%= f.label :name %> |
|
15 |
+ <%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %> |
|
16 |
+ </div> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ |
|
20 |
+ <div class="row"> |
|
21 |
+ <div class="col-md-8"> |
|
22 |
+ <div class="form-group"> |
|
23 |
+ <%= f.label :description, "Optional Description" %> |
|
24 |
+ <%= f.text_area :description, :rows => 10, :class => 'form-control', :placeholder => "Optionally describe what this Scenario will do. If this will be public, you should also include some contact information." %> |
|
25 |
+ </div> |
|
26 |
+ |
|
27 |
+ <div class="checkbox"> |
|
28 |
+ <%= f.label :public do %> |
|
29 |
+ <%= f.check_box :public %> Share this Scenario publicly |
|
30 |
+ <% end %> |
|
31 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="When selected, this Scenario and all Agents in it will be made public. An export URL will be available to share with other Huginn users. Be very careful that you do not have secret credentials stored in these Agents' options. Instead, use Credentials by reference."></span> |
|
32 |
+ </div> |
|
33 |
+ |
|
34 |
+ </div> |
|
35 |
+ </div> |
|
36 |
+ |
|
37 |
+ <div class="row"> |
|
38 |
+ <div class="col-md-4"> |
|
39 |
+ <div class="form-group"> |
|
40 |
+ <div> |
|
41 |
+ <%= f.label :agents %> |
|
42 |
+ <%= f.select(:agent_ids, |
|
43 |
+ options_for_select(current_user.agents.pluck(:name, :id), @scenario.agent_ids), |
|
44 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
45 |
+ </div> |
|
46 |
+ </div> |
|
47 |
+ </div> |
|
48 |
+ </div> |
|
49 |
+ |
|
50 |
+ <div class="row"> |
|
51 |
+ <div class="col-md-12"> |
|
52 |
+ <div class='form-actions' style='clear: both'> |
|
53 |
+ <%= f.submit "Save Scenario", :class => "btn btn-primary" %> |
|
54 |
+ </div> |
|
55 |
+ </div> |
|
56 |
+ </div> |
|
57 |
+<% end %> |
@@ -0,0 +1,21 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Edit Scenario |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <%= render 'form' %> |
|
11 |
+ |
|
12 |
+ <hr> |
|
13 |
+ |
|
14 |
+ <div class="row"> |
|
15 |
+ <div class="col-md-12"> |
|
16 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ </div> |
|
20 |
+ </div> |
|
21 |
+</div> |
@@ -0,0 +1,50 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Your Scenarios |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <blockquote>Scenarios are named groups of Agents. Scenarios allow you to organize your agents, |
|
11 |
+ and to import and export sets of Agents to share.</blockquote> |
|
12 |
+ |
|
13 |
+ <table class='table table-striped'> |
|
14 |
+ <tr> |
|
15 |
+ <th>Name</th> |
|
16 |
+ <th>Agents</th> |
|
17 |
+ <th>Public</th> |
|
18 |
+ <th></th> |
|
19 |
+ </tr> |
|
20 |
+ |
|
21 |
+ <% @scenarios.each do |scenario| %> |
|
22 |
+ <tr> |
|
23 |
+ <td> |
|
24 |
+ <%= link_to(scenario.name, scenario) %> |
|
25 |
+ </td> |
|
26 |
+ <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> |
|
27 |
+ <td><%= scenario.public? ? "yes" : "no" %></td> |
|
28 |
+ <td> |
|
29 |
+ <div class="btn-group btn-group-xs" style="float: right"> |
|
30 |
+ <%= link_to 'Show', scenario, class: "btn btn-default" %> |
|
31 |
+ <%= link_to 'Edit', edit_scenario_path(scenario), class: "btn btn-default" %> |
|
32 |
+ <%= link_to 'Share', share_scenario_path(scenario), class: "btn btn-default" %> |
|
33 |
+ <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: { confirm: "This will remove the '#{scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
34 |
+ </div> |
|
35 |
+ </td> |
|
36 |
+ </tr> |
|
37 |
+ <% end %> |
|
38 |
+ </table> |
|
39 |
+ |
|
40 |
+ <%= paginate @scenarios, :theme => 'twitter-bootstrap' %> |
|
41 |
+ |
|
42 |
+ <br/> |
|
43 |
+ |
|
44 |
+ <div class="btn-group"> |
|
45 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %> |
|
46 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %> |
|
47 |
+ </div> |
|
48 |
+ </div> |
|
49 |
+ </div> |
|
50 |
+</div> |
@@ -0,0 +1,21 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Create a new Scenario |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <%= render 'form' %> |
|
11 |
+ |
|
12 |
+ <hr> |
|
13 |
+ |
|
14 |
+ <div class="row"> |
|
15 |
+ <div class="col-md-12"> |
|
16 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ </div> |
|
20 |
+ </div> |
|
21 |
+</div> |
@@ -0,0 +1,33 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2> |
|
6 |
+ </div> |
|
7 |
+ |
|
8 |
+ <p> |
|
9 |
+ <strong>Please be sure that none of the Agents in this Scenario have sensitive data in their settings before sharing!</strong> |
|
10 |
+ </p> |
|
11 |
+ |
|
12 |
+ <% if @scenario.public? %> |
|
13 |
+ <p> |
|
14 |
+ This Scenario is public. You can <%= link_to "download and share your export file", export_scenario_path(@scenario, :format => :json) %>, or give out this URL: |
|
15 |
+ </p> |
|
16 |
+ |
|
17 |
+ <form onsubmit='return false;'> |
|
18 |
+ <input type='text' class='form-control' value='<%= export_scenario_url(@scenario, :format => :json) %>' onclick="return this.select();"/> |
|
19 |
+ </form> |
|
20 |
+ <% else %> |
|
21 |
+ This Scenario is not public. You can share it by <%= link_to "downloading and sharing your export file", export_scenario_path(@scenario, :format => :json) %>. |
|
22 |
+ <% end %> |
|
23 |
+ |
|
24 |
+ <hr> |
|
25 |
+ |
|
26 |
+ <div class="row"> |
|
27 |
+ <div class="col-md-12"> |
|
28 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %> |
|
29 |
+ </div> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+ </div> |
|
33 |
+</div> |
@@ -0,0 +1,28 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2><span class='label label-info scenario'><%= @scenario.name %></span> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
6 |
+ </div> |
|
7 |
+ |
|
8 |
+ <% if @scenario.description.present? %> |
|
9 |
+ <blockquote><%= @scenario.description %></blockquote> |
|
10 |
+ <% end %> |
|
11 |
+ |
|
12 |
+ <%= render 'agents/table', :returnTo => scenario_path(@scenario) %> |
|
13 |
+ |
|
14 |
+ <br/> |
|
15 |
+ |
|
16 |
+ <div class="btn-group"> |
|
17 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
18 |
+ <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, diagram_agents_path(:scenario_id => @scenario.to_param), class: "btn btn-default" %> |
|
19 |
+ <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> |
|
20 |
+ <% if @scenario.source_url.present? %> |
|
21 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %> |
|
22 |
+ <% end %> |
|
23 |
+ <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %> |
|
24 |
+ <%= link_to '<span class="glyphicon glyphicon-trash"></span> Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
25 |
+ </div> |
|
26 |
+ </div> |
|
27 |
+ </div> |
|
28 |
+</div> |
@@ -3,6 +3,7 @@ Huginn::Application.routes.draw do |
||
3 | 3 |
member do |
4 | 4 |
post :run |
5 | 5 |
post :handle_details_post |
6 |
+ put :leave_scenario |
|
6 | 7 |
delete :remove_events |
7 | 8 |
end |
8 | 9 |
|
@@ -26,6 +27,17 @@ Huginn::Application.routes.draw do |
||
26 | 27 |
end |
27 | 28 |
end |
28 | 29 |
|
30 |
+ resources :scenarios do |
|
31 |
+ collection do |
|
32 |
+ resource :scenario_imports, :only => [:new, :create] |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ member do |
|
36 |
+ get :share |
|
37 |
+ get :export |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
29 | 41 |
resources :user_credentials, :except => :show |
30 | 42 |
|
31 | 43 |
resources :services, :only => [:index, :destroy] do |
@@ -0,0 +1,12 @@ |
||
1 |
+class CreateScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ create_table :scenarios do |t| |
|
4 |
+ t.string :name, :null => false |
|
5 |
+ t.integer :user_id, :null => false |
|
6 |
+ |
|
7 |
+ t.timestamps |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ add_column :users, :scenario_count, :integer, :null => false, :default => 0 |
|
11 |
+ end |
|
12 |
+end |
@@ -0,0 +1,10 @@ |
||
1 |
+class CreateScenarioMemberships < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ create_table :scenario_memberships do |t| |
|
4 |
+ t.integer :agent_id, :null => false |
|
5 |
+ t.integer :scenario_id, :null => false |
|
6 |
+ |
|
7 |
+ t.timestamps |
|
8 |
+ end |
|
9 |
+ end |
|
10 |
+end |
@@ -0,0 +1,8 @@ |
||
1 |
+class AddFieldsToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :scenarios, :description, :text |
|
4 |
+ add_column :scenarios, :public, :boolean, :default => false, :null => false |
|
5 |
+ add_column :scenarios, :guid, :string, :null => false |
|
6 |
+ add_column :scenarios, :source_url, :string |
|
7 |
+ end |
|
8 |
+end |
@@ -0,0 +1,7 @@ |
||
1 |
+class AddIndicesToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_index :scenarios, [:user_id, :guid], :unique => true |
|
4 |
+ add_index :scenario_memberships, :agent_id |
|
5 |
+ add_index :scenario_memberships, :scenario_id |
|
6 |
+ end |
|
7 |
+end |
@@ -0,0 +1,15 @@ |
||
1 |
+class AddGuidToAgents < ActiveRecord::Migration |
|
2 |
+ class Agent < ActiveRecord::Base; end |
|
3 |
+ |
|
4 |
+ def change |
|
5 |
+ add_column :agents, :guid, :string |
|
6 |
+ |
|
7 |
+ Agent.find_each do |agent| |
|
8 |
+ agent.update_attribute :guid, SecureRandom.hex |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ change_column_null :agents, :guid, false |
|
12 |
+ |
|
13 |
+ add_index :agents, :guid |
|
14 |
+ end |
|
15 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended that you check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(version: 20140525150140) do |
|
14 |
+ActiveRecord::Schema.define(version: 20140605032822) do |
|
15 | 15 |
|
16 | 16 |
# These are extensions that must be enabled in order to support this database |
17 | 17 |
enable_extension "plpgsql" |
@@ -46,8 +46,10 @@ ActiveRecord::Schema.define(version: 20140525150140) do |
||
46 | 46 |
t.boolean "propagate_immediately", default: false, null: false |
47 | 47 |
t.boolean "disabled", default: false, null: false |
48 | 48 |
t.integer "service_id" |
49 |
+ t.string "guid", null: false |
|
49 | 50 |
end |
50 | 51 |
|
52 |
+ add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree |
|
51 | 53 |
add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree |
52 | 54 |
add_index "agents", ["type"], name: "index_agents_on_type", using: :btree |
53 | 55 |
add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree |
@@ -94,6 +96,29 @@ ActiveRecord::Schema.define(version: 20140525150140) do |
||
94 | 96 |
add_index "links", ["receiver_id", "source_id"], name: "index_links_on_receiver_id_and_source_id", using: :btree |
95 | 97 |
add_index "links", ["source_id", "receiver_id"], name: "index_links_on_source_id_and_receiver_id", using: :btree |
96 | 98 |
|
99 |
+ create_table "scenario_memberships", force: true do |t| |
|
100 |
+ t.integer "agent_id", null: false |
|
101 |
+ t.integer "scenario_id", null: false |
|
102 |
+ t.datetime "created_at" |
|
103 |
+ t.datetime "updated_at" |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ add_index "scenario_memberships", ["agent_id"], name: "index_scenario_memberships_on_agent_id", using: :btree |
|
107 |
+ add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree |
|
108 |
+ |
|
109 |
+ create_table "scenarios", force: true do |t| |
|
110 |
+ t.string "name", null: false |
|
111 |
+ t.integer "user_id", null: false |
|
112 |
+ t.datetime "created_at" |
|
113 |
+ t.datetime "updated_at" |
|
114 |
+ t.text "description" |
|
115 |
+ t.boolean "public", default: false, null: false |
|
116 |
+ t.string "guid", null: false |
|
117 |
+ t.string "source_url" |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree |
|
121 |
+ |
|
97 | 122 |
create_table "services", force: true do |t| |
98 | 123 |
t.integer "user_id" |
99 | 124 |
t.string "provider" |
@@ -141,6 +166,7 @@ ActiveRecord::Schema.define(version: 20140525150140) do |
||
141 | 166 |
t.datetime "locked_at" |
142 | 167 |
t.string "username", null: false |
143 | 168 |
t.string "invitation_code", null: false |
169 |
+ t.integer "scenario_count", default: 0, null: false |
|
144 | 170 |
end |
145 | 171 |
|
146 | 172 |
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree |
@@ -0,0 +1,54 @@ |
||
1 |
+class AgentsExporter |
|
2 |
+ attr_accessor :options |
|
3 |
+ |
|
4 |
+ def initialize(options) |
|
5 |
+ self.options = options |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ # Filename should have no commas or special characters to support Content-Disposition on older browsers. |
|
9 |
+ def filename |
|
10 |
+ ((options[:name] || '').downcase.gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-|-$/, '').presence || 'exported-agents') + ".json" |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def as_json(opts = {}) |
|
14 |
+ { |
|
15 |
+ :name => options[:name].presence || 'No name provided', |
|
16 |
+ :description => options[:description].presence || 'No description provided', |
|
17 |
+ :source_url => options[:source_url], |
|
18 |
+ :guid => options[:guid], |
|
19 |
+ :exported_at => Time.now.utc.iso8601, |
|
20 |
+ :agents => agents.map { |agent| agent_as_json(agent) }, |
|
21 |
+ :links => links |
|
22 |
+ } |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ def agents |
|
26 |
+ options[:agents].to_a |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def links |
|
30 |
+ agent_ids = agents.map(&:id) |
|
31 |
+ |
|
32 |
+ contained_links = agents.map.with_index do |agent, index| |
|
33 |
+ agent.links_as_source.where(:receiver_id => agent_ids).map do |link| |
|
34 |
+ { :source => index, :receiver => agent_ids.index(link.receiver_id) } |
|
35 |
+ end |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ contained_links.flatten.compact |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def agent_as_json(agent) |
|
42 |
+ { |
|
43 |
+ :type => agent.type, |
|
44 |
+ :name => agent.name, |
|
45 |
+ :disabled => agent.disabled, |
|
46 |
+ :guid => agent.guid, |
|
47 |
+ :options => agent.options |
|
48 |
+ }.tap do |options| |
|
49 |
+ options[:schedule] = agent.schedule if agent.can_be_scheduled? |
|
50 |
+ options[:keep_events_for] = agent.keep_events_for if agent.can_create_events? |
|
51 |
+ options[:propagate_immediately] = agent.propagate_immediately if agent.can_receive_events? |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+end |
@@ -34,6 +34,47 @@ describe AgentsController do |
||
34 | 34 |
end |
35 | 35 |
end |
36 | 36 |
|
37 |
+ describe "POST run" do |
|
38 |
+ it "triggers Agent.async_check with the Agent's ID" do |
|
39 |
+ sign_in users(:bob) |
|
40 |
+ mock(Agent).async_check(agents(:bob_manual_event_agent).id) |
|
41 |
+ post :run, :id => agents(:bob_manual_event_agent).to_param |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ it "can only be accessed by the Agent's owner" do |
|
45 |
+ sign_in users(:jane) |
|
46 |
+ lambda { |
|
47 |
+ post :run, :id => agents(:bob_manual_event_agent).to_param |
|
48 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
49 |
+ end |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ describe "POST remove_events" do |
|
53 |
+ it "deletes all events created by the given Agent" do |
|
54 |
+ sign_in users(:bob) |
|
55 |
+ agent_event = events(:bob_website_agent_event).id |
|
56 |
+ other_event = events(:jane_website_agent_event).id |
|
57 |
+ post :remove_events, :id => agents(:bob_website_agent).to_param |
|
58 |
+ Event.where(:id => agent_event).count.should == 0 |
|
59 |
+ Event.where(:id => other_event).count.should == 1 |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ it "can only be accessed by the Agent's owner" do |
|
63 |
+ sign_in users(:jane) |
|
64 |
+ lambda { |
|
65 |
+ post :remove_events, :id => agents(:bob_website_agent).to_param |
|
66 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
67 |
+ end |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ describe "POST propagate" do |
|
71 |
+ it "runs event propagation for all Agents" do |
|
72 |
+ sign_in users(:bob) |
|
73 |
+ mock.proxy(Agent).receive! |
|
74 |
+ post :propagate |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+ |
|
37 | 78 |
describe "GET show" do |
38 | 79 |
it "only shows Agents for the current user" do |
39 | 80 |
sign_in users(:bob) |
@@ -152,18 +193,80 @@ describe AgentsController do |
||
152 | 193 |
}.should raise_error(ActiveRecord::RecordNotFound) |
153 | 194 |
end |
154 | 195 |
|
196 |
+ it "accepts JSON requests" do |
|
197 |
+ sign_in users(:bob) |
|
198 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :format => :json |
|
199 |
+ agents(:bob_website_agent).reload.name.should == "New name" |
|
200 |
+ JSON.parse(response.body)['name'].should == "New name" |
|
201 |
+ response.should be_success |
|
202 |
+ end |
|
203 |
+ |
|
155 | 204 |
it "will not accept Agent sources owned by other users" do |
156 | 205 |
sign_in users(:bob) |
157 | 206 |
post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id]) |
158 | 207 |
assigns(:agent).should have(1).errors_on(:sources) |
159 | 208 |
end |
160 | 209 |
|
210 |
+ it "will not accept Scenarios owned by other users" do |
|
211 |
+ sign_in users(:bob) |
|
212 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:scenario_ids => [scenarios(:jane_weather).id]) |
|
213 |
+ assigns(:agent).should have(1).errors_on(:scenarios) |
|
214 |
+ end |
|
215 |
+ |
|
161 | 216 |
it "shows errors" do |
162 | 217 |
sign_in users(:bob) |
163 | 218 |
post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "") |
164 | 219 |
assigns(:agent).should have(1).errors_on(:name) |
165 | 220 |
response.should render_template("edit") |
166 | 221 |
end |
222 |
+ |
|
223 |
+ describe "redirecting back" do |
|
224 |
+ before do |
|
225 |
+ sign_in users(:bob) |
|
226 |
+ end |
|
227 |
+ |
|
228 |
+ it "can redirect back to the show path" do |
|
229 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "show" |
|
230 |
+ response.should redirect_to(agent_path(agents(:bob_website_agent))) |
|
231 |
+ end |
|
232 |
+ |
|
233 |
+ it "redirect back to the index path by default" do |
|
234 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name") |
|
235 |
+ response.should redirect_to(agents_path) |
|
236 |
+ end |
|
237 |
+ |
|
238 |
+ it "accepts return paths to scenarios" do |
|
239 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenarios/2" |
|
240 |
+ response.should redirect_to("/scenarios/2") |
|
241 |
+ end |
|
242 |
+ |
|
243 |
+ it "sanitizes return paths" do |
|
244 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenar" |
|
245 |
+ response.should redirect_to(agents_path) |
|
246 |
+ |
|
247 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "http://google.com" |
|
248 |
+ response.should redirect_to(agents_path) |
|
249 |
+ |
|
250 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "javascript:alert(1)" |
|
251 |
+ response.should redirect_to(agents_path) |
|
252 |
+ end |
|
253 |
+ end |
|
254 |
+ end |
|
255 |
+ |
|
256 |
+ describe "PUT leave_scenario" do |
|
257 |
+ it "removes an Agent from the given Scenario for the current user" do |
|
258 |
+ sign_in users(:bob) |
|
259 |
+ |
|
260 |
+ agents(:bob_weather_agent).scenarios.should include(scenarios(:bob_weather)) |
|
261 |
+ put :leave_scenario, :id => agents(:bob_weather_agent).to_param, :scenario_id => scenarios(:bob_weather).to_param |
|
262 |
+ agents(:bob_weather_agent).scenarios.should_not include(scenarios(:bob_weather)) |
|
263 |
+ |
|
264 |
+ Scenario.where(:id => scenarios(:bob_weather).id).should exist |
|
265 |
+ |
|
266 |
+ lambda { |
|
267 |
+ put :leave_scenario, :id => agents(:jane_weather_agent).to_param, :scenario_id => scenarios(:jane_weather).to_param |
|
268 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
269 |
+ end |
|
167 | 270 |
end |
168 | 271 |
|
169 | 272 |
describe "DELETE destroy" do |
@@ -0,0 +1,26 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImportsController do |
|
4 |
+ before do |
|
5 |
+ sign_in users(:bob) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ describe "GET new" do |
|
9 |
+ it "initializes a new ScenarioImport and renders new" do |
|
10 |
+ get :new |
|
11 |
+ assigns(:scenario_import).should be_a(ScenarioImport) |
|
12 |
+ response.should render_template(:new) |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ describe "POST create" do |
|
17 |
+ it "initializes a ScenarioImport for current_user, passing in params" do |
|
18 |
+ post :create, :scenario_import => { :url => "bad url" } |
|
19 |
+ assigns(:scenario_import).user.should == users(:bob) |
|
20 |
+ assigns(:scenario_import).url.should == "bad url" |
|
21 |
+ assigns(:scenario_import).should_not be_valid |
|
22 |
+ response.should render_template(:new) |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+end |
|
26 |
+ |
@@ -0,0 +1,152 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenariosController do |
|
4 |
+ def valid_attributes(options = {}) |
|
5 |
+ { :name => "some_name" }.merge(options) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ before do |
|
9 |
+ sign_in users(:bob) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ describe "GET index" do |
|
13 |
+ it "only returns Scenarios for the current user" do |
|
14 |
+ get :index |
|
15 |
+ assigns(:scenarios).all? {|i| i.user.should == users(:bob) }.should be_true |
|
16 |
+ end |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ describe "GET show" do |
|
20 |
+ it "only shows Scenarios for the current user" do |
|
21 |
+ get :show, :id => scenarios(:bob_weather).to_param |
|
22 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
23 |
+ |
|
24 |
+ lambda { |
|
25 |
+ get :show, :id => scenarios(:jane_weather).to_param |
|
26 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "loads Agents for the requested Scenario" do |
|
30 |
+ get :show, :id => scenarios(:bob_weather).to_param |
|
31 |
+ assigns(:agents).pluck(:id).should eq(scenarios(:bob_weather).agents.pluck(:id)) |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ describe "GET share" do |
|
36 |
+ it "only displays Scenario share information for the current user" do |
|
37 |
+ get :share, :id => scenarios(:bob_weather).to_param |
|
38 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
39 |
+ |
|
40 |
+ lambda { |
|
41 |
+ get :share, :id => scenarios(:jane_weather).to_param |
|
42 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ describe "GET export" do |
|
47 |
+ it "returns a JSON file download from an instantiated AgentsExporter" do |
|
48 |
+ get :export, :id => scenarios(:bob_weather).to_param |
|
49 |
+ assigns(:exporter).options[:name].should == scenarios(:bob_weather).name |
|
50 |
+ assigns(:exporter).options[:description].should == scenarios(:bob_weather).description |
|
51 |
+ assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents |
|
52 |
+ assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid |
|
53 |
+ assigns(:exporter).options[:source_url].should be_false |
|
54 |
+ response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"' |
|
55 |
+ response.headers['Content-Type'].should == 'application/json; charset=utf-8' |
|
56 |
+ JSON.parse(response.body)["name"].should == scenarios(:bob_weather).name |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ it "only exports private Scenarios for the current user" do |
|
60 |
+ get :export, :id => scenarios(:bob_weather).to_param |
|
61 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
62 |
+ |
|
63 |
+ lambda { |
|
64 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
65 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
66 |
+ end |
|
67 |
+ |
|
68 |
+ describe "public exports" do |
|
69 |
+ before do |
|
70 |
+ scenarios(:jane_weather).update_attribute :public, true |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ it "exports public scenarios for other users when logged in" do |
|
74 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
75 |
+ assigns(:scenario).should eq(scenarios(:jane_weather)) |
|
76 |
+ assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) |
|
77 |
+ end |
|
78 |
+ |
|
79 |
+ it "exports public scenarios for other users when logged out" do |
|
80 |
+ sign_out :user |
|
81 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
82 |
+ assigns(:scenario).should eq(scenarios(:jane_weather)) |
|
83 |
+ assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) |
|
84 |
+ end |
|
85 |
+ end |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ describe "GET edit" do |
|
89 |
+ it "only shows Scenarios for the current user" do |
|
90 |
+ get :edit, :id => scenarios(:bob_weather).to_param |
|
91 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
92 |
+ |
|
93 |
+ lambda { |
|
94 |
+ get :edit, :id => scenarios(:jane_weather).to_param |
|
95 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
96 |
+ end |
|
97 |
+ end |
|
98 |
+ |
|
99 |
+ describe "POST create" do |
|
100 |
+ it "creates Scenarios for the current user" do |
|
101 |
+ expect { |
|
102 |
+ post :create, :scenario => valid_attributes |
|
103 |
+ }.to change { users(:bob).scenarios.count }.by(1) |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ it "shows errors" do |
|
107 |
+ expect { |
|
108 |
+ post :create, :scenario => valid_attributes(:name => "") |
|
109 |
+ }.not_to change { users(:bob).scenarios.count } |
|
110 |
+ assigns(:scenario).should have(1).errors_on(:name) |
|
111 |
+ response.should render_template("new") |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+ it "will not create Scenarios for other users" do |
|
115 |
+ expect { |
|
116 |
+ post :create, :scenario => valid_attributes(:user_id => users(:jane).id) |
|
117 |
+ }.to raise_error(ActiveModel::MassAssignmentSecurity::Error) |
|
118 |
+ end |
|
119 |
+ end |
|
120 |
+ |
|
121 |
+ describe "PUT update" do |
|
122 |
+ it "updates attributes on Scenarios for the current user" do |
|
123 |
+ post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" } |
|
124 |
+ response.should redirect_to(scenario_path(scenarios(:bob_weather))) |
|
125 |
+ scenarios(:bob_weather).reload.name.should == "new_name" |
|
126 |
+ scenarios(:bob_weather).should be_public |
|
127 |
+ |
|
128 |
+ lambda { |
|
129 |
+ post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" } |
|
130 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
131 |
+ scenarios(:jane_weather).reload.name.should_not == "new_name" |
|
132 |
+ end |
|
133 |
+ |
|
134 |
+ it "shows errors" do |
|
135 |
+ post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "" } |
|
136 |
+ assigns(:scenario).should have(1).errors_on(:name) |
|
137 |
+ response.should render_template("edit") |
|
138 |
+ end |
|
139 |
+ end |
|
140 |
+ |
|
141 |
+ describe "DELETE destroy" do |
|
142 |
+ it "destroys only Scenarios owned by the current user" do |
|
143 |
+ expect { |
|
144 |
+ delete :destroy, :id => scenarios(:bob_weather).to_param |
|
145 |
+ }.to change(Scenario, :count).by(-1) |
|
146 |
+ |
|
147 |
+ lambda { |
|
148 |
+ delete :destroy, :id => scenarios(:jane_weather).to_param |
|
149 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
150 |
+ end |
|
151 |
+ end |
|
152 |
+end |
@@ -4,6 +4,7 @@ jane_website_agent: |
||
4 | 4 |
events_count: 1 |
5 | 5 |
schedule: "5pm" |
6 | 6 |
name: "ZKCD" |
7 |
+ guid: <%= SecureRandom.hex %> |
|
7 | 8 |
options: <%= { |
8 | 9 |
:url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", |
9 | 10 |
:expected_update_period_in_days => 2, |
@@ -20,6 +21,7 @@ bob_website_agent: |
||
20 | 21 |
events_count: 1 |
21 | 22 |
schedule: "midnight" |
22 | 23 |
name: "ZKCD" |
24 |
+ guid: <%= SecureRandom.hex %> |
|
23 | 25 |
options: <%= { |
24 | 26 |
:url => "http://xkcd.com", |
25 | 27 |
:expected_update_period_in_days => 2, |
@@ -35,6 +37,7 @@ bob_weather_agent: |
||
35 | 37 |
user: bob |
36 | 38 |
schedule: "midnight" |
37 | 39 |
name: "SF Weather" |
40 |
+ guid: <%= SecureRandom.hex %> |
|
38 | 41 |
keep_events_for: 45 |
39 | 42 |
options: <%= { |
40 | 43 |
:location => 94102, |
@@ -48,6 +51,7 @@ jane_weather_agent: |
||
48 | 51 |
user: jane |
49 | 52 |
schedule: "midnight" |
50 | 53 |
name: "SF Weather" |
54 |
+ guid: <%= SecureRandom.hex %> |
|
51 | 55 |
keep_events_for: 30 |
52 | 56 |
options: <%= { |
53 | 57 |
:location => 94103, |
@@ -60,6 +64,7 @@ jane_rain_notifier_agent: |
||
60 | 64 |
type: Agents::TriggerAgent |
61 | 65 |
user: jane |
62 | 66 |
name: "Jane's Rain Watcher" |
67 |
+ guid: <%= SecureRandom.hex %> |
|
63 | 68 |
options: <%= { |
64 | 69 |
:expected_receive_period_in_days => "2", |
65 | 70 |
:rules => [{ |
@@ -74,6 +79,7 @@ bob_rain_notifier_agent: |
||
74 | 79 |
type: Agents::TriggerAgent |
75 | 80 |
user: bob |
76 | 81 |
name: "Bob's Rain Watcher" |
82 |
+ guid: <%= SecureRandom.hex %> |
|
77 | 83 |
options: <%= { |
78 | 84 |
:expected_receive_period_in_days => "2", |
79 | 85 |
:rules => [{ |
@@ -88,6 +94,7 @@ bob_twitter_user_agent: |
||
88 | 94 |
type: Agents::TwitterUserAgent |
89 | 95 |
user: bob |
90 | 96 |
name: "Bob's Twitter User Watcher" |
97 |
+ guid: <%= SecureRandom.hex %> |
|
91 | 98 |
options: <%= { |
92 | 99 |
:username => "tectonic", |
93 | 100 |
:expected_update_period_in_days => "2", |
@@ -101,8 +108,10 @@ bob_manual_event_agent: |
||
101 | 108 |
type: Agents::ManualEventAgent |
102 | 109 |
user: bob |
103 | 110 |
name: "Bob's event testing agent" |
111 |
+ guid: <%= SecureRandom.hex %> |
|
104 | 112 |
|
105 | 113 |
bob_basecamp_agent: |
106 | 114 |
type: Agents::BasecampAgent |
107 | 115 |
user: bob |
108 |
- service: generic |
|
116 |
+ service: generic |
|
117 |
+ guid: <%= SecureRandom.hex %> |
@@ -0,0 +1,15 @@ |
||
1 |
+jane_weather_agent_scenario_membership: |
|
2 |
+ agent: jane_weather_agent |
|
3 |
+ scenario: jane_weather |
|
4 |
+ |
|
5 |
+jane_rain_notifier_agent_scenario_membership: |
|
6 |
+ agent: jane_rain_notifier_agent |
|
7 |
+ scenario: jane_weather |
|
8 |
+ |
|
9 |
+bob_weather_agent_scenario_membership: |
|
10 |
+ agent: bob_weather_agent |
|
11 |
+ scenario: bob_weather |
|
12 |
+ |
|
13 |
+bob_rain_notifier_agent_scenario_membership: |
|
14 |
+ agent: bob_rain_notifier_agent |
|
15 |
+ scenario: bob_weather |
@@ -0,0 +1,13 @@ |
||
1 |
+jane_weather: |
|
2 |
+ name: Jane's weather alert Scenario |
|
3 |
+ user: jane |
|
4 |
+ description: Jane's weather alert system |
|
5 |
+ public: false |
|
6 |
+ guid: random-guid-generated-by-bob |
|
7 |
+ |
|
8 |
+bob_weather: |
|
9 |
+ name: Bob's weather alert Scenario |
|
10 |
+ user: bob |
|
11 |
+ description: Bob's weather alert system |
|
12 |
+ public: false |
|
13 |
+ guid: random-guid-generated-by-jane |
@@ -4,8 +4,10 @@ bob: |
||
4 | 4 |
email: "bob@example.com" |
5 | 5 |
username: bob |
6 | 6 |
invitation_code: <%= User::INVITATION_CODES.last %> |
7 |
+ scenario_count: 1 |
|
7 | 8 |
|
8 | 9 |
jane: |
9 | 10 |
email: "jane@example.com" |
10 | 11 |
username: jane |
11 |
- invitation_code: <%= User::INVITATION_CODES.last %> |
|
12 |
+ invitation_code: <%= User::INVITATION_CODES.last %> |
|
13 |
+ scenario_count: 1 |
@@ -0,0 +1,61 @@ |
||
1 |
+# encoding: utf-8 |
|
2 |
+ |
|
3 |
+require 'spec_helper' |
|
4 |
+ |
|
5 |
+describe AgentsExporter do |
|
6 |
+ describe "#as_json" do |
|
7 |
+ let(:name) { "My set of Agents" } |
|
8 |
+ let(:description) { "These Agents work together nicely!" } |
|
9 |
+ let(:guid) { "some-guid" } |
|
10 |
+ let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" } |
|
11 |
+ let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] } |
|
12 |
+ let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) } |
|
13 |
+ |
|
14 |
+ it "outputs a structure containing name, description, the date, all agents & their links" do |
|
15 |
+ data = exporter.as_json |
|
16 |
+ data[:name].should == name |
|
17 |
+ data[:description].should == description |
|
18 |
+ data[:source_url].should == source_url |
|
19 |
+ data[:guid].should == guid |
|
20 |
+ Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) |
|
21 |
+ data[:links].should == [{ :source => 0, :receiver => 1 }] |
|
22 |
+ data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } |
|
23 |
+ data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true |
|
24 |
+ |
|
25 |
+ data[:agents][0].should_not have_key(:propagate_immediately) # can't receive events |
|
26 |
+ data[:agents][1].should_not have_key(:schedule) # can't be scheduled |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "does not output links to other agents outside of the incoming set" do |
|
30 |
+ Link.create!(:source_id => agents(:jane_weather_agent).id, :receiver_id => agents(:jane_website_agent).id) |
|
31 |
+ Link.create!(:source_id => agents(:jane_website_agent).id, :receiver_id => agents(:jane_rain_notifier_agent).id) |
|
32 |
+ |
|
33 |
+ exporter.as_json[:links].should == [{ :source => 0, :receiver => 1 }] |
|
34 |
+ end |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ describe "#filename" do |
|
38 |
+ it "strips special characters" do |
|
39 |
+ AgentsExporter.new(:name => "ƏfooƐƕƺbar").filename.should == "foo-bar.json" |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "strips punctuation" do |
|
43 |
+ AgentsExporter.new(:name => "foo,bar").filename.should == "foo-bar.json" |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "strips leading and trailing dashes" do |
|
47 |
+ AgentsExporter.new(:name => ",foo,").filename.should == "foo.json" |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ it "has a default when options[:name] is nil" do |
|
51 |
+ AgentsExporter.new(:name => nil).filename.should == "exported-agents.json" |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ it "has a default when the result is empty" do |
|
55 |
+ AgentsExporter.new(:name => "").filename.should == "exported-agents.json" |
|
56 |
+ AgentsExporter.new(:name => "Ə").filename.should == "exported-agents.json" |
|
57 |
+ AgentsExporter.new(:name => "-").filename.should == "exported-agents.json" |
|
58 |
+ AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json" |
|
59 |
+ end |
|
60 |
+ end |
|
61 |
+end |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/working_helpers' |
|
3 | 2 |
|
4 | 3 |
describe Agent do |
5 | 4 |
it_behaves_like WorkingHelpers |
@@ -122,6 +121,17 @@ describe Agent do |
||
122 | 121 |
stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true } |
123 | 122 |
end |
124 | 123 |
|
124 |
+ describe Agents::SomethingSource do |
|
125 |
+ let(:new_instance) do |
|
126 |
+ agent = Agents::SomethingSource.new(:name => "some agent") |
|
127 |
+ agent.user = users(:bob) |
|
128 |
+ agent |
|
129 |
+ end |
|
130 |
+ |
|
131 |
+ it_behaves_like LiquidInterpolatable |
|
132 |
+ it_behaves_like HasGuid |
|
133 |
+ end |
|
134 |
+ |
|
125 | 135 |
describe ".default_schedule" do |
126 | 136 |
it "stores the default on the class" do |
127 | 137 |
Agents::SomethingSource.default_schedule.should == "2pm" |
@@ -480,6 +490,23 @@ describe Agent do |
||
480 | 490 |
agent.should have(0).errors_on(:sources) |
481 | 491 |
end |
482 | 492 |
|
493 |
+ it "should not allow scenarios owned by other people" do |
|
494 |
+ agent = Agents::SomethingSource.new(:name => "something") |
|
495 |
+ agent.user = users(:bob) |
|
496 |
+ |
|
497 |
+ agent.scenario_ids = [scenarios(:bob_weather).id] |
|
498 |
+ agent.should have(0).errors_on(:scenarios) |
|
499 |
+ |
|
500 |
+ agent.scenario_ids = [scenarios(:bob_weather).id, scenarios(:jane_weather).id] |
|
501 |
+ agent.should have(1).errors_on(:scenarios) |
|
502 |
+ |
|
503 |
+ agent.scenario_ids = [scenarios(:jane_weather).id] |
|
504 |
+ agent.should have(1).errors_on(:scenarios) |
|
505 |
+ |
|
506 |
+ agent.user = users(:jane) |
|
507 |
+ agent.should have(0).errors_on(:scenarios) |
|
508 |
+ end |
|
509 |
+ |
|
483 | 510 |
it "validates keep_events_for" do |
484 | 511 |
agent = Agents::SomethingSource.new(:name => "something") |
485 | 512 |
agent.user = users(:bob) |
@@ -1,11 +1,8 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 | 3 |
require 'spec_helper' |
4 |
-require 'models/concerns/liquid_interpolatable' |
|
5 | 4 |
|
6 | 5 |
describe Agents::DataOutputAgent do |
7 |
- it_behaves_like LiquidInterpolatable |
|
8 |
- |
|
9 | 6 |
let(:agent) do |
10 | 7 |
_agent = Agents::DataOutputAgent.new(:name => 'My Data Output Agent') |
11 | 8 |
_agent.options = _agent.default_options.merge('secrets' => ['secret1', 'secret2'], 'events_to_show' => 2) |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HipchatAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before(:each) do |
8 | 5 |
@valid_params = { |
9 | 6 |
'auth_token' => 'token', |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HumanTaskAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before do |
8 | 5 |
@checker = Agents::HumanTaskAgent.new(:name => "my human task agent") |
9 | 6 |
@checker.options = @checker.default_options |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::JabberAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
let(:sent) { [] } |
8 | 5 |
let(:config) { |
9 | 6 |
{ |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PeakDetectorAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before do |
8 | 5 |
@valid_params = { |
9 | 6 |
'name' => "my peak detector agent", |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PushbulletAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before(:each) do |
8 | 5 |
@valid_params = { |
9 | 6 |
'api_key' => 'token', |
@@ -17,7 +17,7 @@ describe Agents::ShellCommandAgent do |
||
17 | 17 |
@event = Event.new |
18 | 18 |
@event.agent = agents(:jane_weather_agent) |
19 | 19 |
@event.payload = { |
20 |
- :command => "ls" |
|
20 |
+ :cmd => "ls" |
|
21 | 21 |
} |
22 | 22 |
@event.save! |
23 | 23 |
|
@@ -78,13 +78,14 @@ describe Agents::ShellCommandAgent do |
||
78 | 78 |
|
79 | 79 |
describe "#receive" do |
80 | 80 |
before do |
81 |
- stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] } |
|
81 |
+ stub(@checker).run_command(@valid_path, @event.payload[:cmd]) { ["fake ls output", "", 0] } |
|
82 | 82 |
end |
83 | 83 |
|
84 | 84 |
it "creates events" do |
85 |
+ @checker.options[:command] = "{{cmd}}" |
|
85 | 86 |
@checker.receive([@event]) |
86 | 87 |
Event.last.payload[:path].should == @valid_path |
87 |
- Event.last.payload[:command].should == @event.payload[:command] |
|
88 |
+ Event.last.payload[:command].should == @event.payload[:cmd] |
|
88 | 89 |
Event.last.payload[:output].should == "fake ls output" |
89 | 90 |
end |
90 | 91 |
|
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::SlackAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before(:each) do |
8 | 5 |
@valid_params = { |
9 | 6 |
'auth_token' => 'token', |
@@ -51,7 +48,8 @@ describe Agents::SlackAgent do |
||
51 | 48 |
username: @event.payload[:username] |
52 | 49 |
) |
53 | 50 |
end |
54 |
- expect(@checker.receive([@event])).to_not raise_error |
|
51 |
+ |
|
52 |
+ lambda { @checker.receive([@event]) }.should_not raise_error |
|
55 | 53 |
end |
56 | 54 |
end |
57 | 55 |
|
@@ -1,10 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 |
- |
|
4 | 2 |
|
5 | 3 |
describe Agents::TranslationAgent do |
6 |
- it_behaves_like LiquidInterpolatable |
|
7 |
- |
|
8 | 4 |
before do |
9 | 5 |
@valid_params = { |
10 | 6 |
:name => "somename", |
@@ -1,9 +1,6 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::TriggerAgent do |
5 |
- it_behaves_like LiquidInterpolatable |
|
6 |
- |
|
7 | 4 |
before do |
8 | 5 |
@valid_params = { |
9 | 6 |
'name' => "my trigger agent", |
@@ -0,0 +1,411 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImport do |
|
4 |
+ let(:user) { users(:bob) } |
|
5 |
+ let(:guid) { "somescenarioguid" } |
|
6 |
+ let(:description) { "This is a cool Huginn Scenario that does something useful!" } |
|
7 |
+ let(:name) { "A useful Scenario" } |
|
8 |
+ let(:source_url) { "http://example.com/scenarios/2/export.json" } |
|
9 |
+ let(:weather_agent_options) { |
|
10 |
+ { |
|
11 |
+ 'api_key' => 'some-api-key', |
|
12 |
+ 'location' => '12345' |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+ let(:trigger_agent_options) { |
|
16 |
+ { |
|
17 |
+ 'expected_receive_period_in_days' => 2, |
|
18 |
+ 'rules' => [{ |
|
19 |
+ 'type' => "regex", |
|
20 |
+ 'value' => "rain|storm", |
|
21 |
+ 'path' => "conditions", |
|
22 |
+ }], |
|
23 |
+ 'message' => "Looks like rain!" |
|
24 |
+ } |
|
25 |
+ } |
|
26 |
+ let(:valid_parsed_weather_agent_data) do |
|
27 |
+ { |
|
28 |
+ :type => "Agents::WeatherAgent", |
|
29 |
+ :name => "a weather agent", |
|
30 |
+ :schedule => "5pm", |
|
31 |
+ :keep_events_for => 14, |
|
32 |
+ :disabled => true, |
|
33 |
+ :guid => "a-weather-agent", |
|
34 |
+ :options => weather_agent_options |
|
35 |
+ } |
|
36 |
+ end |
|
37 |
+ let(:valid_parsed_trigger_agent_data) do |
|
38 |
+ { |
|
39 |
+ :type => "Agents::TriggerAgent", |
|
40 |
+ :name => "listen for weather", |
|
41 |
+ :keep_events_for => 0, |
|
42 |
+ :propagate_immediately => true, |
|
43 |
+ :disabled => false, |
|
44 |
+ :guid => "a-trigger-agent", |
|
45 |
+ :options => trigger_agent_options |
|
46 |
+ } |
|
47 |
+ end |
|
48 |
+ let(:valid_parsed_data) do |
|
49 |
+ { |
|
50 |
+ :name => name, |
|
51 |
+ :description => description, |
|
52 |
+ :guid => guid, |
|
53 |
+ :source_url => source_url, |
|
54 |
+ :exported_at => 2.days.ago.utc.iso8601, |
|
55 |
+ :agents => [ |
|
56 |
+ valid_parsed_weather_agent_data, |
|
57 |
+ valid_parsed_trigger_agent_data |
|
58 |
+ ], |
|
59 |
+ :links => [ |
|
60 |
+ { :source => 0, :receiver => 1 } |
|
61 |
+ ] |
|
62 |
+ } |
|
63 |
+ end |
|
64 |
+ let(:valid_data) { valid_parsed_data.to_json } |
|
65 |
+ let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json } |
|
66 |
+ |
|
67 |
+ describe "initialization" do |
|
68 |
+ it "is initialized with an attributes hash" do |
|
69 |
+ ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ describe "validations" do |
|
74 |
+ subject do |
|
75 |
+ _import = ScenarioImport.new |
|
76 |
+ _import.set_user(user) |
|
77 |
+ _import |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ it "is not valid when none of file, url, or data are present" do |
|
81 |
+ subject.should_not be_valid |
|
82 |
+ subject.should have(1).error_on(:base) |
|
83 |
+ subject.errors[:base].should include("Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ describe "data" do |
|
87 |
+ it "should be invalid with invalid data" do |
|
88 |
+ subject.data = invalid_data |
|
89 |
+ subject.should_not be_valid |
|
90 |
+ subject.should have(1).error_on(:base) |
|
91 |
+ |
|
92 |
+ subject.data = "foo" |
|
93 |
+ subject.should_not be_valid |
|
94 |
+ subject.should have(1).error_on(:base) |
|
95 |
+ |
|
96 |
+ # It also clears the data when invalid |
|
97 |
+ subject.data.should be_nil |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ it "should be valid with valid data" do |
|
101 |
+ subject.data = valid_data |
|
102 |
+ subject.should be_valid |
|
103 |
+ end |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ describe "url" do |
|
107 |
+ it "should be invalid with an unreasonable URL" do |
|
108 |
+ subject.url = "foo" |
|
109 |
+ subject.should_not be_valid |
|
110 |
+ subject.should have(1).error_on(:url) |
|
111 |
+ subject.errors[:url].should include("appears to be invalid") |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+ it "should be invalid when the referenced url doesn't contain a scenario" do |
|
115 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data) |
|
116 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
117 |
+ subject.should_not be_valid |
|
118 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
119 |
+ end |
|
120 |
+ |
|
121 |
+ it "should be valid when the url points to a valid scenario" do |
|
122 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data) |
|
123 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
124 |
+ subject.should be_valid |
|
125 |
+ end |
|
126 |
+ end |
|
127 |
+ |
|
128 |
+ describe "file" do |
|
129 |
+ it "should be invalid when the uploaded file doesn't contain a scenario" do |
|
130 |
+ subject.file = StringIO.new("foo") |
|
131 |
+ subject.should_not be_valid |
|
132 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
133 |
+ |
|
134 |
+ subject.file = StringIO.new(invalid_data) |
|
135 |
+ subject.should_not be_valid |
|
136 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
137 |
+ end |
|
138 |
+ |
|
139 |
+ it "should be valid with a valid uploaded scenario" do |
|
140 |
+ subject.file = StringIO.new(valid_data) |
|
141 |
+ subject.should be_valid |
|
142 |
+ end |
|
143 |
+ end |
|
144 |
+ end |
|
145 |
+ |
|
146 |
+ describe "#dangerous?" do |
|
147 |
+ it "returns false on most Agents" do |
|
148 |
+ ScenarioImport.new(:data => valid_data).should_not be_dangerous |
|
149 |
+ end |
|
150 |
+ |
|
151 |
+ it "returns true if a ShellCommandAgent is present" do |
|
152 |
+ valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" |
|
153 |
+ ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous |
|
154 |
+ end |
|
155 |
+ end |
|
156 |
+ |
|
157 |
+ describe "#import and #generate_diff" do |
|
158 |
+ let(:scenario_import) do |
|
159 |
+ _import = ScenarioImport.new(:data => valid_data) |
|
160 |
+ _import.set_user users(:bob) |
|
161 |
+ _import |
|
162 |
+ end |
|
163 |
+ |
|
164 |
+ context "when this scenario has never been seen before" do |
|
165 |
+ describe "#import" do |
|
166 |
+ it "makes a new scenario" do |
|
167 |
+ lambda { |
|
168 |
+ scenario_import.import(:skip_agents => true) |
|
169 |
+ }.should change { users(:bob).scenarios.count }.by(1) |
|
170 |
+ |
|
171 |
+ scenario_import.scenario.name.should == name |
|
172 |
+ scenario_import.scenario.description.should == description |
|
173 |
+ scenario_import.scenario.guid.should == guid |
|
174 |
+ scenario_import.scenario.source_url.should == source_url |
|
175 |
+ scenario_import.scenario.public.should be_false |
|
176 |
+ end |
|
177 |
+ |
|
178 |
+ it "creates the Agents" do |
|
179 |
+ lambda { |
|
180 |
+ scenario_import.import |
|
181 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
182 |
+ |
|
183 |
+ weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") |
|
184 |
+ trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") |
|
185 |
+ |
|
186 |
+ weather_agent.name.should == "a weather agent" |
|
187 |
+ weather_agent.schedule.should == "5pm" |
|
188 |
+ weather_agent.keep_events_for.should == 14 |
|
189 |
+ weather_agent.propagate_immediately.should be_false |
|
190 |
+ weather_agent.should be_disabled |
|
191 |
+ weather_agent.memory.should be_empty |
|
192 |
+ weather_agent.options.should == weather_agent_options |
|
193 |
+ |
|
194 |
+ trigger_agent.name.should == "listen for weather" |
|
195 |
+ trigger_agent.sources.should == [weather_agent] |
|
196 |
+ trigger_agent.schedule.should be_nil |
|
197 |
+ trigger_agent.keep_events_for.should == 0 |
|
198 |
+ trigger_agent.propagate_immediately.should be_true |
|
199 |
+ trigger_agent.should_not be_disabled |
|
200 |
+ trigger_agent.memory.should be_empty |
|
201 |
+ trigger_agent.options.should == trigger_agent_options |
|
202 |
+ end |
|
203 |
+ |
|
204 |
+ it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do |
|
205 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
206 |
+ |
|
207 |
+ lambda { |
|
208 |
+ scenario_import.import |
|
209 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
210 |
+ end |
|
211 |
+ end |
|
212 |
+ |
|
213 |
+ describe "#generate_diff" do |
|
214 |
+ it "returns AgentDiff objects for the incoming Agents" do |
|
215 |
+ scenario_import.should be_valid |
|
216 |
+ |
|
217 |
+ agent_diffs = scenario_import.agent_diffs |
|
218 |
+ |
|
219 |
+ weather_agent_diff = agent_diffs[0] |
|
220 |
+ trigger_agent_diff = agent_diffs[1] |
|
221 |
+ |
|
222 |
+ valid_parsed_weather_agent_data.each do |key, value| |
|
223 |
+ if key == :type |
|
224 |
+ value = value.split("::").last |
|
225 |
+ end |
|
226 |
+ weather_agent_diff.should respond_to(key) |
|
227 |
+ field = weather_agent_diff.send(key) |
|
228 |
+ field.should be_a(ScenarioImport::AgentDiff::FieldDiff) |
|
229 |
+ field.incoming.should == value |
|
230 |
+ field.updated.should == value |
|
231 |
+ field.current.should be_nil |
|
232 |
+ end |
|
233 |
+ weather_agent_diff.should_not respond_to(:propagate_immediately) |
|
234 |
+ |
|
235 |
+ valid_parsed_trigger_agent_data.each do |key, value| |
|
236 |
+ if key == :type |
|
237 |
+ value = value.split("::").last |
|
238 |
+ end |
|
239 |
+ trigger_agent_diff.should respond_to(key) |
|
240 |
+ field = trigger_agent_diff.send(key) |
|
241 |
+ field.should be_a(ScenarioImport::AgentDiff::FieldDiff) |
|
242 |
+ field.incoming.should == value |
|
243 |
+ field.updated.should == value |
|
244 |
+ field.current.should be_nil |
|
245 |
+ end |
|
246 |
+ trigger_agent_diff.should_not respond_to(:schedule) |
|
247 |
+ end |
|
248 |
+ end |
|
249 |
+ end |
|
250 |
+ |
|
251 |
+ context "when an a scenario already exists with the given guid" do |
|
252 |
+ let!(:existing_scenario) do |
|
253 |
+ _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario", :description => "something") |
|
254 |
+ _existing_scenerio.guid = guid |
|
255 |
+ _existing_scenerio.save! |
|
256 |
+ |
|
257 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
258 |
+ agents(:bob_weather_agent).scenarios << _existing_scenerio |
|
259 |
+ |
|
260 |
+ _existing_scenerio |
|
261 |
+ end |
|
262 |
+ |
|
263 |
+ describe "#import" do |
|
264 |
+ it "uses the existing scenario, updating its data" do |
|
265 |
+ lambda { |
|
266 |
+ scenario_import.import(:skip_agents => true) |
|
267 |
+ scenario_import.scenario.should == existing_scenario |
|
268 |
+ }.should_not change { users(:bob).scenarios.count } |
|
269 |
+ |
|
270 |
+ existing_scenario.reload |
|
271 |
+ existing_scenario.guid.should == guid |
|
272 |
+ existing_scenario.description.should == description |
|
273 |
+ existing_scenario.name.should == name |
|
274 |
+ existing_scenario.source_url.should == source_url |
|
275 |
+ existing_scenario.public.should be_false |
|
276 |
+ end |
|
277 |
+ |
|
278 |
+ it "updates any existing agents in the scenario, and makes new ones as needed" do |
|
279 |
+ scenario_import.should be_valid |
|
280 |
+ |
|
281 |
+ lambda { |
|
282 |
+ scenario_import.import |
|
283 |
+ }.should change { users(:bob).agents.count }.by(1) # One, because the weather agent already existed. |
|
284 |
+ |
|
285 |
+ weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") |
|
286 |
+ trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") |
|
287 |
+ |
|
288 |
+ weather_agent.should == agents(:bob_weather_agent) |
|
289 |
+ |
|
290 |
+ weather_agent.name.should == "a weather agent" |
|
291 |
+ weather_agent.schedule.should == "5pm" |
|
292 |
+ weather_agent.keep_events_for.should == 14 |
|
293 |
+ weather_agent.propagate_immediately.should be_false |
|
294 |
+ weather_agent.should be_disabled |
|
295 |
+ weather_agent.memory.should be_empty |
|
296 |
+ weather_agent.options.should == weather_agent_options |
|
297 |
+ |
|
298 |
+ trigger_agent.name.should == "listen for weather" |
|
299 |
+ trigger_agent.sources.should == [weather_agent] |
|
300 |
+ trigger_agent.schedule.should be_nil |
|
301 |
+ trigger_agent.keep_events_for.should == 0 |
|
302 |
+ trigger_agent.propagate_immediately.should be_true |
|
303 |
+ trigger_agent.should_not be_disabled |
|
304 |
+ trigger_agent.memory.should be_empty |
|
305 |
+ trigger_agent.options.should == trigger_agent_options |
|
306 |
+ end |
|
307 |
+ |
|
308 |
+ it "honors updates coming from the UI" do |
|
309 |
+ scenario_import.merges = { |
|
310 |
+ "0" => { |
|
311 |
+ "name" => "updated name", |
|
312 |
+ "schedule" => "6pm", |
|
313 |
+ "keep_events_for" => "2", |
|
314 |
+ "disabled" => "false", |
|
315 |
+ "options" => weather_agent_options.merge("api_key" => "foo").to_json |
|
316 |
+ } |
|
317 |
+ } |
|
318 |
+ |
|
319 |
+ scenario_import.should be_valid |
|
320 |
+ |
|
321 |
+ scenario_import.import.should be_true |
|
322 |
+ |
|
323 |
+ weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") |
|
324 |
+ weather_agent.name.should == "updated name" |
|
325 |
+ weather_agent.schedule.should == "6pm" |
|
326 |
+ weather_agent.keep_events_for.should == 2 |
|
327 |
+ weather_agent.should_not be_disabled |
|
328 |
+ weather_agent.options.should == weather_agent_options.merge("api_key" => "foo") |
|
329 |
+ end |
|
330 |
+ |
|
331 |
+ it "adds errors when updated agents are invalid" do |
|
332 |
+ scenario_import.merges = { |
|
333 |
+ "0" => { |
|
334 |
+ "name" => "", |
|
335 |
+ "schedule" => "foo", |
|
336 |
+ "keep_events_for" => "2", |
|
337 |
+ "options" => weather_agent_options.merge("api_key" => "").to_json |
|
338 |
+ } |
|
339 |
+ } |
|
340 |
+ |
|
341 |
+ scenario_import.import.should be_false |
|
342 |
+ |
|
343 |
+ errors = scenario_import.errors.full_messages.to_sentence |
|
344 |
+ errors.should =~ /Name can't be blank/ |
|
345 |
+ errors.should =~ /api_key is required/ |
|
346 |
+ errors.should =~ /Schedule is not a valid schedule/ |
|
347 |
+ end |
|
348 |
+ end |
|
349 |
+ |
|
350 |
+ describe "#generate_diff" do |
|
351 |
+ it "returns AgentDiff objects that include 'current' values from any agents that already exist" do |
|
352 |
+ agent_diffs = scenario_import.agent_diffs |
|
353 |
+ weather_agent_diff = agent_diffs[0] |
|
354 |
+ trigger_agent_diff = agent_diffs[1] |
|
355 |
+ |
|
356 |
+ # Already exists |
|
357 |
+ weather_agent_diff.agent.should == agents(:bob_weather_agent) |
|
358 |
+ valid_parsed_weather_agent_data.each do |key, value| |
|
359 |
+ next if key == :type |
|
360 |
+ weather_agent_diff.send(key).current.should == agents(:bob_weather_agent).send(key) |
|
361 |
+ end |
|
362 |
+ |
|
363 |
+ # Doesn't exist yet |
|
364 |
+ valid_parsed_trigger_agent_data.each do |key, value| |
|
365 |
+ trigger_agent_diff.send(key).current.should be_nil |
|
366 |
+ end |
|
367 |
+ end |
|
368 |
+ |
|
369 |
+ it "sets the 'updated' FieldDiff values based on any feedback from the user" do |
|
370 |
+ scenario_import.merges = { |
|
371 |
+ "0" => { |
|
372 |
+ "name" => "a new name", |
|
373 |
+ "schedule" => "6pm", |
|
374 |
+ "keep_events_for" => "2", |
|
375 |
+ "disabled" => "true", |
|
376 |
+ "options" => weather_agent_options.merge("api_key" => "foo").to_json |
|
377 |
+ }, |
|
378 |
+ "1" => { |
|
379 |
+ "name" => "another new name" |
|
380 |
+ } |
|
381 |
+ } |
|
382 |
+ |
|
383 |
+ scenario_import.should be_valid |
|
384 |
+ |
|
385 |
+ agent_diffs = scenario_import.agent_diffs |
|
386 |
+ weather_agent_diff = agent_diffs[0] |
|
387 |
+ trigger_agent_diff = agent_diffs[1] |
|
388 |
+ |
|
389 |
+ weather_agent_diff.name.current.should == agents(:bob_weather_agent).name |
|
390 |
+ weather_agent_diff.name.incoming.should == valid_parsed_weather_agent_data[:name] |
|
391 |
+ weather_agent_diff.name.updated.should == "a new name" |
|
392 |
+ |
|
393 |
+ weather_agent_diff.schedule.updated.should == "6pm" |
|
394 |
+ weather_agent_diff.keep_events_for.updated.should == "2" |
|
395 |
+ weather_agent_diff.disabled.updated.should == "true" |
|
396 |
+ weather_agent_diff.options.updated.should == weather_agent_options.merge("api_key" => "foo") |
|
397 |
+ end |
|
398 |
+ |
|
399 |
+ it "adds errors on validation when updated options are unparsable" do |
|
400 |
+ scenario_import.merges = { |
|
401 |
+ "0" => { |
|
402 |
+ "options" => '{' |
|
403 |
+ } |
|
404 |
+ } |
|
405 |
+ scenario_import.should_not be_valid |
|
406 |
+ scenario_import.should have(1).error_on(:base) |
|
407 |
+ end |
|
408 |
+ end |
|
409 |
+ end |
|
410 |
+ end |
|
411 |
+end |
@@ -0,0 +1,43 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Scenario do |
|
4 |
+ let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } |
|
5 |
+ |
|
6 |
+ it_behaves_like HasGuid |
|
7 |
+ |
|
8 |
+ describe "validations" do |
|
9 |
+ before do |
|
10 |
+ new_instance.should be_valid |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ it "validates the presence of name" do |
|
14 |
+ new_instance.name = '' |
|
15 |
+ new_instance.should_not be_valid |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it "validates the presence of user" do |
|
19 |
+ new_instance.user = nil |
|
20 |
+ new_instance.should_not be_valid |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it "only allows Agents owned by user" do |
|
24 |
+ new_instance.agent_ids = [agents(:bob_website_agent).id] |
|
25 |
+ new_instance.should be_valid |
|
26 |
+ |
|
27 |
+ new_instance.agent_ids = [agents(:jane_website_agent).id] |
|
28 |
+ new_instance.should_not be_valid |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe "counters" do |
|
33 |
+ it "maintains a counter cache on user" do |
|
34 |
+ lambda { |
|
35 |
+ new_instance.save! |
|
36 |
+ }.should change { users(:bob).reload.scenario_count }.by(1) |
|
37 |
+ |
|
38 |
+ lambda { |
|
39 |
+ new_instance.destroy |
|
40 |
+ }.should change { users(:bob).reload.scenario_count }.by(-1) |
|
41 |
+ end |
|
42 |
+ end |
|
43 |
+end |
@@ -0,0 +1,12 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for HasGuid do |
|
4 |
+ it "gets created before_save, but only if it's not present" do |
|
5 |
+ instance = new_instance |
|
6 |
+ instance.guid.should be_nil |
|
7 |
+ instance.save! |
|
8 |
+ instance.guid.should_not be_nil |
|
9 |
+ |
|
10 |
+ lambda { instance.save! }.should_not change { instance.reload.guid } |
|
11 |
+ end |
|
12 |
+end |
@@ -28,7 +28,7 @@ shared_examples_for LiquidInterpolatable do |
||
28 | 28 |
} |
29 | 29 |
end |
30 | 30 |
|
31 |
- it "hsould work with arrays", focus: true do |
|
31 |
+ it "should work with arrays", focus: true do |
|
32 | 32 |
@checker.options = {"value" => ["{{variable}}", "Much array", "Hey, {{hello_world}}"]} |
33 | 33 |
@checker.interpolate_options(@checker.options, @event.payload).should == { |
34 | 34 |
"value" => ["hello", "Much array", "Hey, Hello world"] |
@@ -53,6 +53,7 @@ shared_examples_for LiquidInterpolatable do |
||
53 | 53 |
@checker.interpolate_string("{{variable}} you", @event.payload).should == "hello you" |
54 | 54 |
end |
55 | 55 |
end |
56 |
+ |
|
56 | 57 |
describe "liquid tags" do |
57 | 58 |
it "should work with existing credentials" do |
58 | 59 |
@checker.interpolate_string("{% credential aws_key %}", {}).should == '2222222222-jane' |
@@ -3,7 +3,7 @@ require 'spec_helper' |
||
3 | 3 |
shared_examples_for WorkingHelpers do |
4 | 4 |
describe "recent_error_logs?" do |
5 | 5 |
it "returns true if last_error_log_at is near last_event_at" do |
6 |
- agent = Agent.new |
|
6 |
+ agent = described_class.new |
|
7 | 7 |
|
8 | 8 |
agent.last_error_log_at = 10.minutes.ago |
9 | 9 |
agent.last_event_at = 10.minutes.ago |
@@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do |
||
26 | 26 |
agent.recent_error_logs?.should be_false |
27 | 27 |
end |
28 | 28 |
end |
29 |
+ |
|
29 | 30 |
describe "received_event_without_error?" do |
30 | 31 |
before do |
31 |
- @agent = Agent.new |
|
32 |
+ @agent = described_class.new |
|
32 | 33 |
end |
33 | 34 |
|
34 | 35 |
it "should return false until the first event was received" do |
@@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do |
||
49 | 50 |
@agent.received_event_without_error?.should == true |
50 | 51 |
end |
51 | 52 |
end |
52 |
- |
|
53 | 53 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
/* |
2 |
- Copyright (c) 2013, Andrew Cantino |
|
2 |
+ Copyright (c) 2014, Andrew Cantino |
|
3 | 3 |
Copyright (c) 2009, Andrew Cantino & Kyle Maxwell |
4 | 4 |
|
5 | 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
@@ -23,8 +23,8 @@ |
||
23 | 23 |
|
24 | 24 |
|
25 | 25 |
|
26 |
- You will probably need to tell the editor where to find its add and delete images. In your |
|
27 |
- code before you make the editor, do something like this: |
|
26 |
+ You will probably need to tell the editor where to find its 'add' and 'delete' images. In your |
|
27 |
+ code, before you make the editor, do something like this: |
|
28 | 28 |
JSONEditor.prototype.ADD_IMG = '/javascripts/jsoneditor/add.png'; |
29 | 29 |
JSONEditor.prototype.DELETE_IMG = '/javascripts/jsoneditor/delete.png'; |
30 | 30 |
|
@@ -36,504 +36,529 @@ |
||
36 | 36 |
*/ |
37 | 37 |
|
38 | 38 |
|
39 |
-function JSONEditorBase(options) { |
|
40 |
- if (!options) options = {}; |
|
41 |
- this.builderShowing = true; |
|
42 |
- this.ADD_IMG = options.ADD_IMG || 'lib/images/add.png'; |
|
43 |
- this.DELETE_IMG = options.DELETE_IMG || 'lib/images/delete.png'; |
|
44 |
- this.functionButtonsEnabled = false; |
|
45 |
- this._doTruncation = true; |
|
46 |
- this._showWipe = options.showWipe; |
|
47 |
-} |
|
48 |
- |
|
49 |
-function JSONEditor(wrapped, width, height) { |
|
50 |
- this.history = []; |
|
51 |
- this.historyPointer = -1; |
|
52 |
- if (wrapped == null || (wrapped.get && wrapped.get(0) == null)) throw "Must provide an element to wrap."; |
|
53 |
- var width = width || 600; |
|
54 |
- var height = height || 300; |
|
55 |
- this.wrapped = $(wrapped); |
|
56 |
- |
|
57 |
- this.wrapped.wrap('<div class="json-editor"></div>'); |
|
58 |
- this.container = $(this.wrapped.parent()); |
|
59 |
- this.container.width(width).height(height); |
|
60 |
- this.wrapped.width(width).height(height); |
|
61 |
- this.wrapped.hide(); |
|
62 |
- this.container.css("position", "relative"); |
|
63 |
- this.doAutoFocus = false; |
|
64 |
- this.editingUnfocused(); |
|
65 |
- |
|
66 |
- this.rebuild(); |
|
67 |
- var self = this; |
|
68 |
- this.container.focus(function(){ |
|
69 |
- $(this).children('textarea').height(self.container.height() - self.functionButtons.height() - 5); |
|
70 |
- $(this).children('.builder').height(self.container.height() - self.functionButtons.height() - 10); |
|
71 |
- }); |
|
72 |
- |
|
73 |
- return this; |
|
74 |
-} |
|
75 |
-JSONEditor.prototype = new JSONEditorBase(); |
|
76 |
- |
|
77 |
-JSONEditor.prototype.braceUI = function(key, struct) { |
|
78 |
- var self = this; |
|
79 |
- return $('<a class="icon" href="#"><strong>{</strong></a>').click(function(e) { |
|
80 |
- struct[key] = { "??": struct[key] }; |
|
81 |
- self.doAutoFocus = true; |
|
82 |
- self.rebuild(); |
|
83 |
- return false; |
|
84 |
- }); |
|
85 |
-}; |
|
86 |
- |
|
87 |
-JSONEditor.prototype.bracketUI = function(key, struct) { |
|
88 |
- var self = this; |
|
89 |
- return $('<a class="icon" href="#"><strong>[</a>').click(function(e) { |
|
90 |
- struct[key] = [ struct[key] ]; |
|
91 |
- self.doAutoFocus = true; |
|
92 |
- self.rebuild(); |
|
93 |
- return false; |
|
94 |
- }); |
|
95 |
-}; |
|
96 |
- |
|
97 |
-JSONEditor.prototype.deleteUI = function(key, struct, fullDelete) { |
|
98 |
- var self = this; |
|
99 |
- return $('<a class="icon" href="#" title="delete"><img src="' + this.DELETE_IMG + '" border=0/></a>').click(function(e) { |
|
100 |
- if (!fullDelete) { |
|
101 |
- var didSomething = false; |
|
102 |
- if (struct[key] instanceof Array) { |
|
103 |
- if(struct[key].length > 0) { |
|
104 |
- struct[key] = struct[key][0]; |
|
105 |
- didSomething = true; |
|
39 |
+(function() { |
|
40 |
+ |
|
41 |
+ window.JSONEditor = (function() { |
|
42 |
+ |
|
43 |
+ function JSONEditor(wrapped, options) { |
|
44 |
+ if (options == null) { |
|
45 |
+ options = {}; |
|
46 |
+ } |
|
47 |
+ this.builderShowing = true; |
|
48 |
+ this.ADD_IMG || (this.ADD_IMG = options.ADD_IMG || 'lib/images/add.png'); |
|
49 |
+ this.DELETE_IMG || (this.DELETE_IMG = options.DELETE_IMG || 'lib/images/delete.png'); |
|
50 |
+ this.functionButtonsEnabled = false; |
|
51 |
+ this._doTruncation = true; |
|
52 |
+ this._showWipe = options.showWipe; |
|
53 |
+ this.history = []; |
|
54 |
+ this.historyPointer = -1; |
|
55 |
+ if (wrapped === null || (wrapped.get && wrapped.get(0) === null)) { |
|
56 |
+ throw "Must provide an element to wrap."; |
|
57 |
+ } |
|
58 |
+ this.wrapped = $(wrapped); |
|
59 |
+ this.wrapped.wrap('<div class="json-editor"></div>'); |
|
60 |
+ this.container = $(this.wrapped.parent()); |
|
61 |
+ this.wrapped.hide(); |
|
62 |
+ this.container.css("position", "relative"); |
|
63 |
+ this.doAutoFocus = false; |
|
64 |
+ this.editingUnfocused(); |
|
65 |
+ this.rebuild(); |
|
66 |
+ } |
|
67 |
+ |
|
68 |
+ JSONEditor.prototype.braceUI = function(key, struct) { |
|
69 |
+ var _this = this; |
|
70 |
+ return $('<a class="icon" href="#"><strong>{</strong></a>').click(function(e) { |
|
71 |
+ e.preventDefault(); |
|
72 |
+ struct[key] = { |
|
73 |
+ "??": struct[key] |
|
74 |
+ }; |
|
75 |
+ _this.doAutoFocus = true; |
|
76 |
+ return _this.rebuild(); |
|
77 |
+ }); |
|
78 |
+ }; |
|
79 |
+ |
|
80 |
+ JSONEditor.prototype.bracketUI = function(key, struct) { |
|
81 |
+ var _this = this; |
|
82 |
+ return $('<a class="icon" href="#"><strong>[</a>').click(function(e) { |
|
83 |
+ e.preventDefault(); |
|
84 |
+ struct[key] = [struct[key]]; |
|
85 |
+ _this.doAutoFocus = true; |
|
86 |
+ return _this.rebuild(); |
|
87 |
+ }); |
|
88 |
+ }; |
|
89 |
+ |
|
90 |
+ JSONEditor.prototype.deleteUI = function(key, struct, fullDelete) { |
|
91 |
+ var _this = this; |
|
92 |
+ return $("<a class='icon' href='#' title='delete'><img src='" + this.DELETE_IMG + "' border=0 /></a>").click(function(e) { |
|
93 |
+ var didSomething, subkey, subval, _ref; |
|
94 |
+ e.preventDefault(); |
|
95 |
+ if (!fullDelete) { |
|
96 |
+ didSomething = false; |
|
97 |
+ if (struct[key] instanceof Array) { |
|
98 |
+ if (struct[key].length > 0) { |
|
99 |
+ struct[key] = struct[key][0]; |
|
100 |
+ didSomething = true; |
|
101 |
+ } |
|
102 |
+ } else if (struct[key] instanceof Object) { |
|
103 |
+ _ref = struct[key]; |
|
104 |
+ for (subkey in _ref) { |
|
105 |
+ subval = _ref[subkey]; |
|
106 |
+ struct[key] = struct[key][subkey]; |
|
107 |
+ didSomething = true; |
|
108 |
+ break; |
|
109 |
+ } |
|
110 |
+ } |
|
111 |
+ if (didSomething) { |
|
112 |
+ _this.rebuild(); |
|
113 |
+ return; |
|
114 |
+ } |
|
106 | 115 |
} |
107 |
- } else if (struct[key] instanceof Object) { |
|
108 |
- for (var i in struct[key]) { |
|
109 |
- struct[key] = struct[key][i]; |
|
110 |
- didSomething = true; |
|
111 |
- break; |
|
116 |
+ if (struct instanceof Array) { |
|
117 |
+ struct.splice(key, 1); |
|
118 |
+ } else { |
|
119 |
+ delete struct[key]; |
|
120 |
+ } |
|
121 |
+ return _this.rebuild(); |
|
122 |
+ }); |
|
123 |
+ }; |
|
124 |
+ |
|
125 |
+ JSONEditor.prototype.wipeUI = function(key, struct) { |
|
126 |
+ var _this = this; |
|
127 |
+ return $('<a class="icon" href="#" title="wipe"><strong>W</strong></a>').click(function(e) { |
|
128 |
+ e.preventDefault(); |
|
129 |
+ if (struct instanceof Array) { |
|
130 |
+ struct.splice(key, 1); |
|
131 |
+ } else { |
|
132 |
+ delete struct[key]; |
|
133 |
+ } |
|
134 |
+ return _this.rebuild(); |
|
135 |
+ }); |
|
136 |
+ }; |
|
137 |
+ |
|
138 |
+ JSONEditor.prototype.addUI = function(struct) { |
|
139 |
+ var _this = this; |
|
140 |
+ return $("<a class='icon' href='#' title='add'><img src='" + this.ADD_IMG + "' border=0/></a>").click(function(e) { |
|
141 |
+ e.preventDefault(); |
|
142 |
+ if (struct instanceof Array) { |
|
143 |
+ struct.push('??'); |
|
144 |
+ } else { |
|
145 |
+ struct['??'] = '??'; |
|
146 |
+ } |
|
147 |
+ _this.doAutoFocus = true; |
|
148 |
+ return _this.rebuild(); |
|
149 |
+ }); |
|
150 |
+ }; |
|
151 |
+ |
|
152 |
+ JSONEditor.prototype.undo = function() { |
|
153 |
+ if (this.saveStateIfTextChanged()) { |
|
154 |
+ if (this.historyPointer > 0) { |
|
155 |
+ this.historyPointer -= 1; |
|
156 |
+ } |
|
157 |
+ return this.restore(); |
|
158 |
+ } |
|
159 |
+ }; |
|
160 |
+ |
|
161 |
+ JSONEditor.prototype.redo = function() { |
|
162 |
+ if (this.historyPointer + 1 < this.history.length) { |
|
163 |
+ if (this.saveStateIfTextChanged()) { |
|
164 |
+ this.historyPointer += 1; |
|
165 |
+ return this.restore(); |
|
112 | 166 |
} |
113 | 167 |
} |
114 |
- if (didSomething) { |
|
115 |
- self.rebuild(); |
|
168 |
+ }; |
|
169 |
+ |
|
170 |
+ JSONEditor.prototype.showBuilder = function() { |
|
171 |
+ if (this.checkJsonInText()) { |
|
172 |
+ this.setJsonFromText(); |
|
173 |
+ this.rebuild(); |
|
174 |
+ this.wrapped.hide(); |
|
175 |
+ this.builder.show(); |
|
176 |
+ return true; |
|
177 |
+ } else { |
|
178 |
+ alert("Sorry, there appears to be an error in your JSON input. Please fix it before continuing."); |
|
116 | 179 |
return false; |
117 | 180 |
} |
118 |
- } |
|
119 |
- if (struct instanceof Array) { |
|
120 |
- struct.splice(key, 1); |
|
121 |
- } else { |
|
122 |
- delete struct[key]; |
|
123 |
- } |
|
124 |
- self.rebuild(); |
|
125 |
- return false; |
|
126 |
- }); |
|
127 |
-}; |
|
128 |
- |
|
129 |
-JSONEditor.prototype.wipeUI = function(key, struct) { |
|
130 |
- var self = this; |
|
131 |
- return $('<a class="icon" href="#" title="wipe"><strong>W</strong></a>').click(function(e) { |
|
132 |
- if (struct instanceof Array) { |
|
133 |
- struct.splice(key, 1); |
|
134 |
- } else { |
|
135 |
- delete struct[key]; |
|
136 |
- } |
|
137 |
- self.rebuild(); |
|
138 |
- return false; |
|
139 |
- }); |
|
140 |
-}; |
|
141 |
- |
|
142 |
-JSONEditor.prototype.addUI = function(struct) { |
|
143 |
- var self = this; |
|
144 |
- return $('<a class="icon" href="#" title="add"><img src="' + this.ADD_IMG + '" border=0/></a>').click(function(e) { |
|
145 |
- if (struct instanceof Array) { |
|
146 |
- struct.push('??'); |
|
147 |
- } else { |
|
148 |
- struct['??'] = '??'; |
|
149 |
- } |
|
150 |
- self.doAutoFocus = true; |
|
151 |
- self.rebuild(); |
|
152 |
- return false; |
|
153 |
- }); |
|
154 |
-}; |
|
155 |
- |
|
156 |
-JSONEditor.prototype.undo = function() { |
|
157 |
- if (this.saveStateIfTextChanged()) { |
|
158 |
- if (this.historyPointer > 0) this.historyPointer -= 1; |
|
159 |
- this.restore(); |
|
160 |
- } |
|
161 |
-}; |
|
162 |
- |
|
163 |
-JSONEditor.prototype.redo = function() { |
|
164 |
- if (this.historyPointer + 1 < this.history.length) { |
|
165 |
- if (this.saveStateIfTextChanged()) { |
|
166 |
- this.historyPointer += 1; |
|
167 |
- this.restore(); |
|
168 |
- } |
|
169 |
- } |
|
170 |
-}; |
|
171 |
- |
|
172 |
-JSONEditor.prototype.showBuilder = function() { |
|
173 |
- if (this.checkJsonInText()) { |
|
174 |
- this.setJsonFromText(); |
|
175 |
- this.rebuild(); |
|
176 |
- this.wrapped.hide(); |
|
177 |
- this.builder.show(); |
|
178 |
- return true; |
|
179 |
- } else { |
|
180 |
- alert("Sorry, there appears to be an error in your JSON input. Please fix it before continuing."); |
|
181 |
- return false; |
|
182 |
- } |
|
183 |
-}; |
|
184 |
- |
|
185 |
-JSONEditor.prototype.showText = function() { |
|
186 |
- this.builder.hide(); |
|
187 |
- this.wrapped.show(); |
|
188 |
-}; |
|
189 |
- |
|
190 |
-JSONEditor.prototype.toggleBuilder = function() { |
|
191 |
- if(this.builderShowing){ |
|
192 |
- this.showText(); |
|
193 |
- this.builderShowing = !this.builderShowing; |
|
194 |
- } else { |
|
195 |
- if (this.showBuilder()) { |
|
196 |
- this.builderShowing = !this.builderShowing; |
|
181 |
+ }; |
|
182 |
+ |
|
183 |
+ JSONEditor.prototype.showText = function() { |
|
184 |
+ this.builder.hide(); |
|
185 |
+ return this.wrapped.show(); |
|
186 |
+ }; |
|
187 |
+ |
|
188 |
+ JSONEditor.prototype.toggleBuilder = function() { |
|
189 |
+ if (this.builderShowing) { |
|
190 |
+ this.showText(); |
|
191 |
+ return this.builderShowing = !this.builderShowing; |
|
192 |
+ } else { |
|
193 |
+ if (this.showBuilder()) { |
|
194 |
+ return this.builderShowing = !this.builderShowing; |
|
195 |
+ } |
|
197 | 196 |
} |
198 |
- } |
|
199 |
-}; |
|
200 |
- |
|
201 |
-JSONEditor.prototype.showFunctionButtons = function(insider) { |
|
202 |
- if (!insider) this.functionButtonsEnabled = true; |
|
203 |
- if (this.functionButtonsEnabled) if (!this.functionButtons) { |
|
204 |
- this.functionButtons = $('<div class="function_buttons"></div>'); |
|
205 |
- var self = this; |
|
206 |
- this.functionButtons.append($('<a href="#" style="padding-right: 10px;"></a>').click(function() { |
|
207 |
- self.undo(); |
|
208 |
- return false; |
|
209 |
- }).text('Undo')).append($('<a href="#" style="padding-right: 10px;"></a>').click(function() { |
|
210 |
- self.redo(); |
|
211 |
- return false; |
|
212 |
- }).text('Redo')).append($('<a id="toggle_view" href="#" style="padding-right: 10px;"></a>').click(function() { |
|
213 |
- self.toggleBuilder(); |
|
214 |
- return false; |
|
215 |
- }).text('Toggle View')); |
|
216 |
- this.container.prepend(this.functionButtons); |
|
217 |
- this.container.height(this.container.height() + this.functionButtons.height() + 5); |
|
218 |
- } |
|
219 |
- if (this.functionButtons) { |
|
220 |
- this.wrapped.css('top', this.functionButtons.height() + 5 + 'px'); |
|
221 |
- this.builder.css('top', this.functionButtons.height() + 5 + 'px'); |
|
222 |
- } |
|
223 |
-}; |
|
224 |
- |
|
225 |
-JSONEditor.prototype.saveStateIfTextChanged = function() { |
|
226 |
- if (JSON.stringify(this.json, null, 2) != this.wrapped.get(0).value) { |
|
227 |
- if (this.checkJsonInText()) { |
|
228 |
- this.saveState(true); |
|
229 |
- } else { |
|
230 |
- if (confirm("The current JSON is malformed. If you continue, the current JSON will not be saved. Do you wish to continue?")) { |
|
231 |
- this.historyPointer += 1; |
|
197 |
+ }; |
|
198 |
+ |
|
199 |
+ JSONEditor.prototype.showFunctionButtons = function(insider) { |
|
200 |
+ var _this = this; |
|
201 |
+ if (!insider) { |
|
202 |
+ this.functionButtonsEnabled = true; |
|
203 |
+ } |
|
204 |
+ if (this.functionButtonsEnabled && !this.functionButtons) { |
|
205 |
+ this.functionButtons = $('<div class="function_buttons"></div>'); |
|
206 |
+ this.functionButtons.append($('<a href="#" style="padding-right: 10px;">Undo</a>').click(function(e) { |
|
207 |
+ e.preventDefault(); |
|
208 |
+ return _this.undo(); |
|
209 |
+ })); |
|
210 |
+ this.functionButtons.append($('<a href="#" style="padding-right: 10px;">Redo</a>').click(function(e) { |
|
211 |
+ e.preventDefault(); |
|
212 |
+ return _this.redo(); |
|
213 |
+ })); |
|
214 |
+ this.functionButtons.append($('<a id="toggle_view" href="#" style="padding-right: 10px; float: right;">Toggle View</a>').click(function(e) { |
|
215 |
+ e.preventDefault(); |
|
216 |
+ return _this.toggleBuilder(); |
|
217 |
+ })); |
|
218 |
+ return this.container.prepend(this.functionButtons); |
|
219 |
+ } |
|
220 |
+ }; |
|
221 |
+ |
|
222 |
+ JSONEditor.prototype.saveStateIfTextChanged = function() { |
|
223 |
+ if (JSON.stringify(this.json, null, 2) !== this.wrapped.get(0).value) { |
|
224 |
+ if (this.checkJsonInText()) { |
|
225 |
+ this.saveState(true); |
|
226 |
+ } else { |
|
227 |
+ if (confirm("The current JSON is malformed. If you continue, the current JSON will not be saved. Do you wish to continue?")) { |
|
228 |
+ this.historyPointer += 1; |
|
229 |
+ true; |
|
230 |
+ } else { |
|
231 |
+ false; |
|
232 |
+ } |
|
233 |
+ } |
|
234 |
+ } |
|
235 |
+ return true; |
|
236 |
+ }; |
|
237 |
+ |
|
238 |
+ JSONEditor.prototype.restore = function() { |
|
239 |
+ if (this.history[this.historyPointer]) { |
|
240 |
+ this.wrapped.get(0).value = this.history[this.historyPointer]; |
|
241 |
+ return this.rebuild(true); |
|
242 |
+ } |
|
243 |
+ }; |
|
244 |
+ |
|
245 |
+ JSONEditor.prototype.saveState = function(skipStoreText) { |
|
246 |
+ var text; |
|
247 |
+ if (this.json) { |
|
248 |
+ if (!skipStoreText) { |
|
249 |
+ this.storeToText(); |
|
250 |
+ } |
|
251 |
+ text = this.wrapped.get(0).value; |
|
252 |
+ if (this.history[this.historyPointer] !== text) { |
|
253 |
+ this.historyTruncate(); |
|
254 |
+ this.history.push(text); |
|
255 |
+ return this.historyPointer += 1; |
|
256 |
+ } |
|
257 |
+ } |
|
258 |
+ }; |
|
259 |
+ |
|
260 |
+ JSONEditor.prototype.fireChange = function() { |
|
261 |
+ return $(this.wrapped).trigger('change'); |
|
262 |
+ }; |
|
263 |
+ |
|
264 |
+ JSONEditor.prototype.historyTruncate = function() { |
|
265 |
+ if (this.historyPointer + 1 < this.history.length) { |
|
266 |
+ return this.history.splice(this.historyPointer + 1, this.history.length - this.historyPointer); |
|
267 |
+ } |
|
268 |
+ }; |
|
269 |
+ |
|
270 |
+ JSONEditor.prototype.storeToText = function() { |
|
271 |
+ return this.wrapped.get(0).value = JSON.stringify(this.json, null, 2); |
|
272 |
+ }; |
|
273 |
+ |
|
274 |
+ JSONEditor.prototype.getJSONText = function() { |
|
275 |
+ this.rebuild(); |
|
276 |
+ return this.wrapped.get(0).value; |
|
277 |
+ }; |
|
278 |
+ |
|
279 |
+ JSONEditor.prototype.getJSON = function() { |
|
280 |
+ this.rebuild(); |
|
281 |
+ return this.json; |
|
282 |
+ }; |
|
283 |
+ |
|
284 |
+ JSONEditor.prototype.rebuild = function(doNotRefreshText) { |
|
285 |
+ var changed, elem; |
|
286 |
+ if (!this.json) { |
|
287 |
+ this.setJsonFromText(); |
|
288 |
+ } |
|
289 |
+ changed = this.haveThingsChanged(); |
|
290 |
+ if (this.json && !doNotRefreshText) { |
|
291 |
+ this.saveState(); |
|
292 |
+ } |
|
293 |
+ this.cleanBuilder(); |
|
294 |
+ this.setJsonFromText(); |
|
295 |
+ this.alreadyFocused = false; |
|
296 |
+ elem = this.build(this.json, this.builder, null, null, this.json); |
|
297 |
+ this.recoverScrollPosition(); |
|
298 |
+ if (elem && elem.text() === '??' && !this.alreadyFocused && this.doAutoFocus) { |
|
299 |
+ this.alreadyFocused = true; |
|
300 |
+ this.doAutoFocus = false; |
|
301 |
+ elem = elem.find('.editable'); |
|
302 |
+ elem.click(); |
|
303 |
+ elem.find('input').focus().select(); |
|
304 |
+ } |
|
305 |
+ if (changed) { |
|
306 |
+ return this.fireChange(); |
|
307 |
+ } |
|
308 |
+ }; |
|
309 |
+ |
|
310 |
+ JSONEditor.prototype.haveThingsChanged = function() { |
|
311 |
+ return this.json && JSON.stringify(this.json, null, 2) !== this.wrapped.get(0).value; |
|
312 |
+ }; |
|
313 |
+ |
|
314 |
+ JSONEditor.prototype.saveScrollPosition = function() { |
|
315 |
+ return this.oldScrollHeight = this.builder.scrollTop(); |
|
316 |
+ }; |
|
317 |
+ |
|
318 |
+ JSONEditor.prototype.recoverScrollPosition = function() { |
|
319 |
+ return this.builder.scrollTop(this.oldScrollHeight); |
|
320 |
+ }; |
|
321 |
+ |
|
322 |
+ JSONEditor.prototype.setJsonFromText = function() { |
|
323 |
+ if (this.wrapped.get(0).value.length === 0) { |
|
324 |
+ this.wrapped.get(0).value = "{}"; |
|
325 |
+ } |
|
326 |
+ try { |
|
327 |
+ this.wrapped.get(0).value = this.wrapped.get(0).value.replace(/((^|[^\\])(\\\\)*)\\n/g, '$1\\\\n').replace(/((^|[^\\])(\\\\)*)\\t/g, '$1\\\\t'); |
|
328 |
+ return this.json = JSON.parse(this.wrapped.get(0).value); |
|
329 |
+ } catch (e) { |
|
330 |
+ return alert("Got bad JSON from text."); |
|
331 |
+ } |
|
332 |
+ }; |
|
333 |
+ |
|
334 |
+ JSONEditor.prototype.checkJsonInText = function() { |
|
335 |
+ try { |
|
336 |
+ JSON.parse(this.wrapped.get(0).value); |
|
232 | 337 |
return true; |
233 |
- } else { |
|
338 |
+ } catch (e) { |
|
234 | 339 |
return false; |
235 | 340 |
} |
236 |
- } |
|
237 |
- } |
|
238 |
- return true; |
|
239 |
-}; |
|
240 |
- |
|
241 |
-JSONEditor.prototype.restore = function() { |
|
242 |
- if (this.history[this.historyPointer]) { |
|
243 |
- this.wrapped.get(0).value = this.history[this.historyPointer]; |
|
244 |
- this.rebuild(true); |
|
245 |
- } |
|
246 |
-}; |
|
247 |
- |
|
248 |
-JSONEditor.prototype.saveState = function(skipStoreText) { |
|
249 |
- if (this.json) { |
|
250 |
- if (!skipStoreText) this.storeToText(); |
|
251 |
- var text = this.wrapped.get(0).value; |
|
252 |
- if (this.history[this.historyPointer] != text) { |
|
253 |
- this.historyTruncate(); |
|
254 |
- this.history.push(text); |
|
255 |
- this.historyPointer += 1; |
|
256 |
- } |
|
257 |
- } |
|
258 |
-}; |
|
259 |
- |
|
260 |
-JSONEditor.prototype.fireChange = function() { |
|
261 |
- $(this.wrapped).trigger('change'); |
|
262 |
-}; |
|
263 |
- |
|
264 |
-JSONEditor.prototype.historyTruncate = function() { |
|
265 |
- if (this.historyPointer + 1 < this.history.length) { |
|
266 |
- this.history.splice(this.historyPointer + 1, this.history.length - this.historyPointer); |
|
267 |
- } |
|
268 |
-}; |
|
269 |
- |
|
270 |
-JSONEditor.prototype.storeToText = function() { |
|
271 |
- this.wrapped.get(0).value = JSON.stringify(this.json, null, 2); |
|
272 |
-}; |
|
273 |
- |
|
274 |
-JSONEditor.prototype.getJSONText = function() { |
|
275 |
- this.rebuild(); |
|
276 |
- return this.wrapped.get(0).value; |
|
277 |
-}; |
|
278 |
- |
|
279 |
-JSONEditor.prototype.getJSON = function() { |
|
280 |
- this.rebuild(); |
|
281 |
- return this.json; |
|
282 |
-}; |
|
283 |
- |
|
284 |
-JSONEditor.prototype.rebuild = function(doNotRefreshText) { |
|
285 |
- if (!this.json) this.setJsonFromText(); |
|
286 |
- var changed = this.haveThingsChanged(); |
|
287 |
- if (this.json && !doNotRefreshText) { |
|
288 |
- this.saveState(); |
|
289 |
- } |
|
290 |
- this.cleanBuilder(); |
|
291 |
- this.setJsonFromText(); |
|
292 |
- this.alreadyFocused = false; |
|
293 |
- var elem = this.build(this.json, this.builder, null, null, this.json); |
|
294 |
- |
|
295 |
- this.recoverScrollPosition(); |
|
296 |
- |
|
297 |
- // Auto-focus to edit '??' keys and values. |
|
298 |
- if (elem) if (elem.text() == '??' && !this.alreadyFocused && this.doAutoFocus) { |
|
299 |
- this.alreadyFocused = true; |
|
300 |
- this.doAutoFocus = false; |
|
301 |
- |
|
302 |
- elem = elem.find('.editable'); |
|
303 |
- elem.click(); |
|
304 |
- elem.find('input').focus().select(); |
|
305 |
- //still missing a proper scrolling into the selected input |
|
306 |
- } |
|
307 |
- |
|
308 |
- if (changed) this.fireChange(); |
|
309 |
-}; |
|
310 |
- |
|
311 |
-JSONEditor.prototype.haveThingsChanged = function() { |
|
312 |
- return (this.json && JSON.stringify(this.json, null, 2) != this.wrapped.get(0).value); |
|
313 |
-} |
|
314 |
- |
|
315 |
-JSONEditor.prototype.saveScrollPosition = function() { |
|
316 |
- this.oldScrollHeight = this.builder.scrollTop(); |
|
317 |
-}; |
|
318 |
- |
|
319 |
-JSONEditor.prototype.recoverScrollPosition = function() { |
|
320 |
- this.builder.scrollTop(this.oldScrollHeight); |
|
321 |
-}; |
|
322 |
- |
|
323 |
-JSONEditor.prototype.setJsonFromText = function() { |
|
324 |
- if (this.wrapped.get(0).value.length == 0) this.wrapped.get(0).value = "{}"; |
|
325 |
- try { |
|
326 |
- this.wrapped.get(0).value = this.wrapped.get(0).value.replace(/((^|[^\\])(\\\\)*)\\n/g, '$1\\\\n').replace(/((^|[^\\])(\\\\)*)\\t/g, '$1\\\\t'); |
|
327 |
- this.json = JSON.parse(this.wrapped.get(0).value); |
|
328 |
- } catch(e) { |
|
329 |
- alert("Got bad JSON from text."); |
|
330 |
- } |
|
331 |
-}; |
|
332 |
- |
|
333 |
-JSONEditor.prototype.checkJsonInText = function() { |
|
334 |
- try { |
|
335 |
- JSON.parse(this.wrapped.get(0).value); |
|
336 |
- return true; |
|
337 |
- } catch(e) { |
|
338 |
- return false; |
|
339 |
- } |
|
340 |
-}; |
|
341 |
- |
|
342 |
-JSONEditor.prototype.logJSON = function() { |
|
343 |
- console.log(JSON.stringify(this.json, null, 2)); |
|
344 |
-}; |
|
345 |
- |
|
346 |
-JSONEditor.prototype.cleanBuilder = function() { |
|
347 |
- if (!this.builder) { |
|
348 |
- this.builder = $('<div class="builder"></div>'); |
|
349 |
- this.container.append(this.builder); |
|
350 |
- } |
|
351 |
- this.saveScrollPosition(); |
|
352 |
- this.builder.text(''); |
|
353 |
- |
|
354 |
- this.builder.css("position", "absolute").css("top", 0).css("left", 0); |
|
355 |
- this.builder.width(this.wrapped.width()).height(this.wrapped.height()); |
|
356 |
- this.wrapped.css("position", "absolute").css("top", 0).css("left", 0); |
|
357 |
- this.showFunctionButtons("defined"); |
|
358 |
-}; |
|
359 |
- |
|
360 |
-JSONEditor.prototype.updateStruct = function(struct, key, val, kind, selectionStart, selectionEnd) { |
|
361 |
- if(kind == 'key') { |
|
362 |
- if (selectionStart && selectionEnd) val = key.substring(0, selectionStart) + val + key.substring(selectionEnd, key.length); |
|
363 |
- struct[val] = struct[key]; |
|
364 |
- |
|
365 |
- //order keys |
|
366 |
- var orderrest = 0; |
|
367 |
- $.each(struct, function (index, value) { |
|
368 |
- //re set rest of the keys |
|
369 |
- if(orderrest & index != val) { |
|
370 |
- var tempval = struct[index]; |
|
371 |
- delete struct[index]; |
|
372 |
- struct[index] = tempval; |
|
373 |
- } |
|
374 |
- if(key == index) { |
|
375 |
- orderrest = 1; |
|
376 |
- } |
|
377 |
- }); |
|
378 |
- // end of order keys |
|
379 |
- |
|
380 |
- if (key != val) delete struct[key]; |
|
381 |
- } else { |
|
382 |
- if (selectionStart && selectionEnd) val = struct[key].substring(0, selectionStart) + val + struct[key].substring(selectionEnd, struct[key].length); |
|
383 |
- struct[key] = val; |
|
384 |
- } |
|
385 |
-}; |
|
386 |
- |
|
387 |
-JSONEditor.prototype.getValFromStruct = function(struct, key, kind) { |
|
388 |
- if(kind == 'key') { |
|
389 |
- return key; |
|
390 |
- } else { |
|
391 |
- return struct[key]; |
|
392 |
- } |
|
393 |
-}; |
|
394 |
- |
|
395 |
-JSONEditor.prototype.doTruncation = function(trueOrFalse) { |
|
396 |
- if (this._doTruncation != trueOrFalse) { |
|
397 |
- this._doTruncation = trueOrFalse; |
|
398 |
- this.rebuild(); |
|
399 |
- } |
|
400 |
-}; |
|
401 |
- |
|
402 |
-JSONEditor.prototype.showWipe = function(trueOrFalse) { |
|
403 |
- if (this._showWipe != trueOrFalse) { |
|
404 |
- this._showWipe = trueOrFalse; |
|
405 |
- this.rebuild(); |
|
406 |
- } |
|
407 |
-}; |
|
408 |
- |
|
409 |
-JSONEditor.prototype.truncate = function(text, length) { |
|
410 |
- if (text.length == 0) return '-empty-'; |
|
411 |
- if(this._doTruncation && text.length > (length || 30)) return(text.substring(0, (length || 30)) + '...'); |
|
412 |
- return text; |
|
413 |
-}; |
|
414 |
- |
|
415 |
-JSONEditor.prototype.replaceLastSelectedFieldIfRecent = function(text) { |
|
416 |
- if (this.lastEditingUnfocusedTime > (new Date()).getTime() - 200) { // Short delay for unfocus to occur. |
|
417 |
- this.setLastEditingFocus(text); |
|
418 |
- this.rebuild(); |
|
419 |
- } |
|
420 |
-}; |
|
421 |
- |
|
422 |
-JSONEditor.prototype.editingUnfocused = function(elem, struct, key, root, kind) { |
|
423 |
- var self = this; |
|
424 |
- |
|
425 |
- var selectionStart = elem && elem.target.selectionStart; |
|
426 |
- var selectionEnd = elem && elem.target.selectionEnd; |
|
427 |
- |
|
428 |
- this.setLastEditingFocus = function(text) { |
|
429 |
- self.updateStruct(struct, key, text, kind, selectionStart, selectionEnd); |
|
430 |
- self.json = root; // Because self.json is a new reference due to rebuild. |
|
431 |
- }; |
|
432 |
- this.lastEditingUnfocusedTime = (new Date()).getTime(); |
|
433 |
-}; |
|
434 |
- |
|
435 |
-JSONEditor.prototype.edit = function(e, key, struct, root, kind){ |
|
436 |
- var self = this; |
|
437 |
- var form = $("<form></form>").css('display', 'inline'); |
|
438 |
- var input = document.createElement("INPUT"); |
|
439 |
- input.value = this.getValFromStruct(struct, key, kind); |
|
440 |
- //alert(this.getValFromStruct(struct, key, kind)); |
|
441 |
- input.className = 'edit_field'; |
|
442 |
- var onblur = function(elem) { |
|
443 |
- var val = input.value; |
|
444 |
- self.updateStruct(struct, key, val, kind); |
|
445 |
- self.editingUnfocused(elem, struct, (kind == 'key' ? val : key), root, kind); |
|
446 |
- e.text(self.truncate(val)); |
|
447 |
- e.get(0).editing = false; |
|
448 |
- if (key != val) self.rebuild(); |
|
449 |
- return false; |
|
450 |
- }; |
|
451 |
- $(input).blur(onblur); |
|
452 |
- $(input).keydown(function(e) { |
|
453 |
- if (e.keyCode == 9 || e.keyCode == 13) { // Tab and enter |
|
454 |
- self.doAutoFocus = true; |
|
455 |
- onblur(e); |
|
456 |
- return false; |
|
457 |
- } |
|
458 |
- }); |
|
459 |
- $(form).submit(function(e) { self.doAutoFocus = true; onblur(e); return false;}).append(input); |
|
460 |
- $(e).html(form); |
|
461 |
- input.focus(); |
|
462 |
-}; |
|
463 |
- |
|
464 |
-JSONEditor.prototype.editable = function(text, key, struct, root, kind) { |
|
465 |
- var self = this; |
|
466 |
- var elem = $('<span class="editable" href="#"></span>').text(this.truncate(text)).click(function(e) { |
|
467 |
- if (!this.editing) { |
|
468 |
- this.editing = true; |
|
469 |
- self.edit($(this), key, struct, root, kind); |
|
470 |
- } |
|
471 |
- return true; |
|
472 |
- }); |
|
473 |
- |
|
474 |
- return elem; |
|
475 |
-} |
|
476 |
- |
|
477 |
-JSONEditor.prototype.build = function(json, node, parent, key, root) { |
|
478 |
- var elem = null; |
|
479 |
- if(json instanceof Array){ |
|
480 |
- var bq = $(document.createElement("BLOCKQUOTE")); |
|
481 |
- bq.append($('<div class="brackets">[</div>')); |
|
482 |
- |
|
483 |
- bq.prepend(this.addUI(json)); |
|
484 |
- if (parent) { |
|
485 |
- if (this._showWipe) bq.prepend(this.wipeUI(key, parent)); |
|
486 |
- bq.prepend(this.deleteUI(key, parent)); |
|
487 |
- } |
|
341 |
+ }; |
|
488 | 342 |
|
489 |
- for(var i = 0; i < json.length; i++) { |
|
490 |
- var innerbq = $(document.createElement("BLOCKQUOTE")); |
|
491 |
- var newElem = this.build(json[i], innerbq, json, i, root); |
|
492 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
493 |
- bq.append(innerbq); |
|
494 |
- } |
|
343 |
+ JSONEditor.prototype.logJSON = function() { |
|
344 |
+ return console.log(JSON.stringify(this.json, null, 2)); |
|
345 |
+ }; |
|
495 | 346 |
|
496 |
- bq.append($('<div class="brackets">]</div>')); |
|
497 |
- node.append(bq); |
|
498 |
- } else if (json instanceof Object) { |
|
499 |
- var bq = $(document.createElement("BLOCKQUOTE")); |
|
500 |
- bq.append($('<div class="bracers">{</div>')); |
|
501 |
- |
|
502 |
- for(var i in json){ |
|
503 |
- var innerbq = $(document.createElement("BLOCKQUOTE")); |
|
504 |
- var newElem = this.editable(i.toString(), i.toString(), json, root, 'key').wrap('<span class="key"></span>').parent(); |
|
505 |
- innerbq.append(newElem); |
|
506 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
507 |
- if (typeof json[i] != 'string' && typeof json[i] != 'number') { |
|
508 |
- innerbq.prepend(this.braceUI(i, json)); |
|
509 |
- innerbq.prepend(this.bracketUI(i, json)); |
|
510 |
- if (this._showWipe) innerbq.prepend(this.wipeUI(i, json)); |
|
511 |
- innerbq.prepend(this.deleteUI(i, json, true)); |
|
347 |
+ JSONEditor.prototype.cleanBuilder = function() { |
|
348 |
+ if (!this.builder) { |
|
349 |
+ this.builder = $('<div class="builder"></div>'); |
|
350 |
+ this.container.append(this.builder); |
|
512 | 351 |
} |
513 |
- innerbq.append($('<span class="colon">: </span>')); |
|
514 |
- newElem = this.build(json[i], innerbq, json, i, root); |
|
515 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
516 |
- bq.append(innerbq); |
|
517 |
- } |
|
352 |
+ this.saveScrollPosition(); |
|
353 |
+ this.builder.text(''); |
|
354 |
+ return this.showFunctionButtons("defined"); |
|
355 |
+ }; |
|
356 |
+ |
|
357 |
+ JSONEditor.prototype.updateStruct = function(struct, key, val, kind, selectionStart, selectionEnd) { |
|
358 |
+ var orderrest; |
|
359 |
+ if (kind === 'key') { |
|
360 |
+ if (selectionStart && selectionEnd) { |
|
361 |
+ val = key.substring(0, selectionStart) + val + key.substring(selectionEnd, key.length); |
|
362 |
+ } |
|
363 |
+ struct[val] = struct[key]; |
|
364 |
+ orderrest = 0; |
|
365 |
+ $.each(struct, function(index, value) { |
|
366 |
+ var tempval; |
|
367 |
+ if (orderrest & index !== val) { |
|
368 |
+ tempval = struct[index]; |
|
369 |
+ delete struct[index]; |
|
370 |
+ struct[index] = tempval; |
|
371 |
+ } |
|
372 |
+ if (key === index) { |
|
373 |
+ return orderrest = 1; |
|
374 |
+ } |
|
375 |
+ }); |
|
376 |
+ if (key !== val) { |
|
377 |
+ return delete struct[key]; |
|
378 |
+ } |
|
379 |
+ } else { |
|
380 |
+ if (selectionStart && selectionEnd) { |
|
381 |
+ val = struct[key].substring(0, selectionStart) + val + struct[key].substring(selectionEnd, struct[key].length); |
|
382 |
+ } |
|
383 |
+ return struct[key] = val; |
|
384 |
+ } |
|
385 |
+ }; |
|
518 | 386 |
|
519 |
- bq.prepend(this.addUI(json)); |
|
520 |
- if (parent) { |
|
521 |
- if (this._showWipe) bq.prepend(this.wipeUI(key, parent)); |
|
522 |
- bq.prepend(this.deleteUI(key, parent)); |
|
523 |
- } |
|
387 |
+ JSONEditor.prototype.getValFromStruct = function(struct, key, kind) { |
|
388 |
+ if (kind === 'key') { |
|
389 |
+ return key; |
|
390 |
+ } else { |
|
391 |
+ return struct[key]; |
|
392 |
+ } |
|
393 |
+ }; |
|
524 | 394 |
|
525 |
- bq.append($('<div class="bracers">}</div>')); |
|
526 |
- node.append(bq); |
|
527 |
- } else { |
|
528 |
- elem = this.editable(json.toString(), key, parent, root, 'value').wrap('<span class="val"></span>').parent(); |
|
529 |
- node.append(elem); |
|
530 |
- node.prepend(this.braceUI(key, parent)); |
|
531 |
- node.prepend(this.bracketUI(key, parent)); |
|
532 |
- if (parent) { |
|
533 |
- if (this._showWipe) node.prepend(this.wipeUI(key, parent)); |
|
534 |
- node.prepend(this.deleteUI(key, parent)); |
|
535 |
- } |
|
395 |
+ JSONEditor.prototype.doTruncation = function(trueOrFalse) { |
|
396 |
+ if (this._doTruncation !== trueOrFalse) { |
|
397 |
+ this._doTruncation = trueOrFalse; |
|
398 |
+ return this.rebuild(); |
|
399 |
+ } |
|
400 |
+ }; |
|
401 |
+ |
|
402 |
+ JSONEditor.prototype.showWipe = function(trueOrFalse) { |
|
403 |
+ if (this._showWipe !== trueOrFalse) { |
|
404 |
+ this._showWipe = trueOrFalse; |
|
405 |
+ return this.rebuild(); |
|
406 |
+ } |
|
407 |
+ }; |
|
408 |
+ |
|
409 |
+ JSONEditor.prototype.truncate = function(text, length) { |
|
410 |
+ if (text.length === 0) { |
|
411 |
+ return '-empty-'; |
|
412 |
+ } |
|
413 |
+ if (this._doTruncation && text.length > (length || 30)) { |
|
414 |
+ return text.substring(0, length || 30) + '...'; |
|
415 |
+ } |
|
416 |
+ return text; |
|
417 |
+ }; |
|
418 |
+ |
|
419 |
+ JSONEditor.prototype.replaceLastSelectedFieldIfRecent = function(text) { |
|
420 |
+ if (this.lastEditingUnfocusedTime > (new Date()).getTime() - 200) { |
|
421 |
+ this.setLastEditingFocus(text); |
|
422 |
+ return this.rebuild(); |
|
423 |
+ } |
|
424 |
+ }; |
|
425 |
+ |
|
426 |
+ JSONEditor.prototype.editingUnfocused = function(elem, struct, key, root, kind) { |
|
427 |
+ var selectionEnd, selectionStart, |
|
428 |
+ _this = this; |
|
429 |
+ selectionStart = elem != null ? elem.selectionStart : void 0; |
|
430 |
+ selectionEnd = elem != null ? elem.selectionEnd : void 0; |
|
431 |
+ this.setLastEditingFocus = function(text) { |
|
432 |
+ _this.updateStruct(struct, key, text, kind, selectionStart, selectionEnd); |
|
433 |
+ return _this.json = root; |
|
434 |
+ }; |
|
435 |
+ return this.lastEditingUnfocusedTime = (new Date()).getTime(); |
|
436 |
+ }; |
|
437 |
+ |
|
438 |
+ JSONEditor.prototype.edit = function($elem, key, struct, root, kind) { |
|
439 |
+ var $input, blurHandler, form, |
|
440 |
+ _this = this; |
|
441 |
+ form = $("<form></form>").css('display', 'inline'); |
|
442 |
+ $input = $("<input />"); |
|
443 |
+ $input.val(this.getValFromStruct(struct, key, kind)); |
|
444 |
+ $input.addClass('edit_field'); |
|
445 |
+ blurHandler = function() { |
|
446 |
+ var val, _ref; |
|
447 |
+ val = $input.val(); |
|
448 |
+ _this.updateStruct(struct, key, val, kind); |
|
449 |
+ _this.editingUnfocused($elem, struct, (_ref = kind === 'key') != null ? _ref : { |
|
450 |
+ val: key |
|
451 |
+ }, root, kind); |
|
452 |
+ $elem.text(_this.truncate(val)); |
|
453 |
+ $elem.get(0).editing = false; |
|
454 |
+ if (key !== val) { |
|
455 |
+ return _this.rebuild(); |
|
456 |
+ } |
|
457 |
+ }; |
|
458 |
+ $input.blur(blurHandler); |
|
459 |
+ $input.keydown(function(e) { |
|
460 |
+ if (e.keyCode === 9 || e.keyCode === 13) { |
|
461 |
+ _this.doAutoFocus = true; |
|
462 |
+ return blurHandler(); |
|
463 |
+ } |
|
464 |
+ }); |
|
465 |
+ $(form).append($input).submit(function(e) { |
|
466 |
+ e.preventDefault(); |
|
467 |
+ _this.doAutoFocus = true; |
|
468 |
+ return blurHandler(); |
|
469 |
+ }); |
|
470 |
+ $elem.html(form); |
|
471 |
+ return $input.focus(); |
|
472 |
+ }; |
|
473 |
+ |
|
474 |
+ JSONEditor.prototype.editable = function(text, key, struct, root, kind) { |
|
475 |
+ var elem, self; |
|
476 |
+ self = this; |
|
477 |
+ elem = $('<span class="editable" href="#"></span>').text(this.truncate(text)).click(function(e) { |
|
478 |
+ if (!this.editing) { |
|
479 |
+ this.editing = true; |
|
480 |
+ self.edit($(this), key, struct, root, kind); |
|
481 |
+ } |
|
482 |
+ return true; |
|
483 |
+ }); |
|
484 |
+ return elem; |
|
485 |
+ }; |
|
486 |
+ |
|
487 |
+ JSONEditor.prototype.build = function(json, node, parent, key, root) { |
|
488 |
+ var bq, elem, i, innerbq, jsonkey, jsonvalue, newElem, _i, _ref; |
|
489 |
+ elem = null; |
|
490 |
+ if (json instanceof Array) { |
|
491 |
+ bq = $(document.createElement("BLOCKQUOTE")); |
|
492 |
+ bq.append($('<div class="brackets">[</div>')); |
|
493 |
+ bq.prepend(this.addUI(json)); |
|
494 |
+ if (parent) { |
|
495 |
+ if (this._showWipe) { |
|
496 |
+ bq.prepend(this.wipeUI(key, parent)); |
|
497 |
+ } |
|
498 |
+ bq.prepend(this.deleteUI(key, parent)); |
|
499 |
+ } |
|
500 |
+ for (i = _i = 0, _ref = json.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { |
|
501 |
+ innerbq = $(document.createElement("BLOCKQUOTE")); |
|
502 |
+ newElem = this.build(json[i], innerbq, json, i, root); |
|
503 |
+ if (newElem && newElem.text() === "??") { |
|
504 |
+ elem = newElem; |
|
505 |
+ } |
|
506 |
+ bq.append(innerbq); |
|
507 |
+ } |
|
508 |
+ bq.append($('<div class="brackets">]</div>')); |
|
509 |
+ node.append(bq); |
|
510 |
+ } else if (json instanceof Object) { |
|
511 |
+ bq = $(document.createElement("BLOCKQUOTE")); |
|
512 |
+ bq.append($('<div class="bracers">{</div>')); |
|
513 |
+ for (jsonkey in json) { |
|
514 |
+ jsonvalue = json[jsonkey]; |
|
515 |
+ innerbq = $(document.createElement("BLOCKQUOTE")); |
|
516 |
+ newElem = this.editable(jsonkey.toString(), jsonkey.toString(), json, root, 'key').wrap('<span class="key"></b>').parent(); |
|
517 |
+ innerbq.append(newElem); |
|
518 |
+ if (newElem && newElem.text() === "??") { |
|
519 |
+ elem = newElem; |
|
520 |
+ } |
|
521 |
+ if (typeof jsonvalue !== 'string') { |
|
522 |
+ innerbq.prepend(this.braceUI(jsonkey, json)); |
|
523 |
+ innerbq.prepend(this.bracketUI(jsonkey, json)); |
|
524 |
+ if (this._showWipe) { |
|
525 |
+ innerbq.prepend(this.wipeUI(jsonkey, json)); |
|
526 |
+ } |
|
527 |
+ innerbq.prepend(this.deleteUI(jsonkey, json, true)); |
|
528 |
+ } |
|
529 |
+ innerbq.append($('<span class="colon">: </span>')); |
|
530 |
+ newElem = this.build(jsonvalue, innerbq, json, jsonkey, root); |
|
531 |
+ if (newElem && newElem.text() === "??") { |
|
532 |
+ elem = newElem; |
|
533 |
+ } |
|
534 |
+ bq.append(innerbq); |
|
535 |
+ } |
|
536 |
+ bq.prepend(this.addUI(json)); |
|
537 |
+ if (parent) { |
|
538 |
+ if (this._showWipe) { |
|
539 |
+ bq.prepend(this.wipeUI(key, parent)); |
|
540 |
+ } |
|
541 |
+ bq.prepend(this.deleteUI(key, parent)); |
|
542 |
+ } |
|
543 |
+ bq.append($('<div class="bracers">}</div>')); |
|
544 |
+ node.append(bq); |
|
545 |
+ } else { |
|
546 |
+ elem = this.editable(json.toString(), key, parent, root, 'value').wrap('<span class="val"></span>').parent(); |
|
547 |
+ node.append(elem); |
|
548 |
+ node.prepend(this.braceUI(key, parent)); |
|
549 |
+ node.prepend(this.bracketUI(key, parent)); |
|
550 |
+ if (parent) { |
|
551 |
+ if (this._showWipe) { |
|
552 |
+ node.prepend(this.wipeUI(key, parent)); |
|
553 |
+ } |
|
554 |
+ node.prepend(this.deleteUI(key, parent)); |
|
555 |
+ } |
|
556 |
+ } |
|
557 |
+ return elem; |
|
558 |
+ }; |
|
559 |
+ |
|
560 |
+ return JSONEditor; |
|
561 |
+ |
|
562 |
+ })(); |
|
536 | 563 |
|
537 |
- } |
|
538 |
- return elem; |
|
539 |
-}; |
|
564 |
+}).call(this); |
@@ -0,0 +1,37 @@ |
||
1 |
+.json-editor { |
|
2 |
+ background-color: #FFF; |
|
3 |
+ position: relative; } |
|
4 |
+ .json-editor textarea { |
|
5 |
+ width: 100%; |
|
6 |
+ font-family: monospace; } |
|
7 |
+ .json-editor .builder { |
|
8 |
+ background-color: white; |
|
9 |
+ overflow: auto; |
|
10 |
+ font-size: 0.9em; } |
|
11 |
+ .json-editor .builder .key { |
|
12 |
+ font-weight: bold; } |
|
13 |
+ .json-editor .builder .key .edit_field { |
|
14 |
+ width: 150px; } |
|
15 |
+ .json-editor .builder .val .edit_field { |
|
16 |
+ width: 200px; } |
|
17 |
+ .json-editor blockquote { |
|
18 |
+ margin: 0; |
|
19 |
+ padding: 0; |
|
20 |
+ clear: both; |
|
21 |
+ padding-left: 7px; } |
|
22 |
+ .json-editor div { |
|
23 |
+ background-color: #cfc; |
|
24 |
+ margin: 1px; |
|
25 |
+ padding: 2px; } |
|
26 |
+ .json-editor .val { |
|
27 |
+ font-style: italic; } |
|
28 |
+ .json-editor .key a, .json-editor .val a { |
|
29 |
+ color: black; |
|
30 |
+ text-decoration: none; } |
|
31 |
+ .json-editor .icon { |
|
32 |
+ display: block; |
|
33 |
+ float: right; |
|
34 |
+ text-decoration: none; |
|
35 |
+ padding-left: 5px; |
|
36 |
+ border: 0; |
|
37 |
+ color: blue; } |
@@ -1,63 +0,0 @@ |
||
1 |
-.json-editor { |
|
2 |
- background-color: #FFF; |
|
3 |
- position: relative; |
|
4 |
- |
|
5 |
- .builder { |
|
6 |
- background-color: white; |
|
7 |
- overflow: auto; |
|
8 |
- font-size: 0.9em; |
|
9 |
- padding-right: 10px; |
|
10 |
- |
|
11 |
- .key { |
|
12 |
- font-weight: bold; |
|
13 |
- |
|
14 |
- .edit_field { |
|
15 |
- width: 80px; |
|
16 |
- } |
|
17 |
- |
|
18 |
- a { |
|
19 |
- color: black; |
|
20 |
- text-decoration: none; |
|
21 |
- } |
|
22 |
- } |
|
23 |
- |
|
24 |
- .val { |
|
25 |
- font-style: italic; |
|
26 |
- |
|
27 |
- .edit_field { |
|
28 |
- width: 180px; |
|
29 |
- } |
|
30 |
- |
|
31 |
- a { |
|
32 |
- color: black; |
|
33 |
- text-decoration: none; |
|
34 |
- } |
|
35 |
- } |
|
36 |
- } |
|
37 |
- |
|
38 |
- blockquote { |
|
39 |
- margin: 0; |
|
40 |
- padding: 0; |
|
41 |
- clear: both; |
|
42 |
- padding-left: 7px; |
|
43 |
- } |
|
44 |
- |
|
45 |
- div { |
|
46 |
- background-color: #cfc; |
|
47 |
- margin: 1px; |
|
48 |
- padding: 2px; |
|
49 |
- } |
|
50 |
- |
|
51 |
- .icon { |
|
52 |
- display: block; |
|
53 |
- float: right; |
|
54 |
- text-decoration: none; |
|
55 |
- padding: 0 5px; |
|
56 |
- border: 0 !important; |
|
57 |
- color: blue; |
|
58 |
- |
|
59 |
- &:hover { |
|
60 |
- background-color: #bbb; |
|
61 |
- } |
|
62 |
- } |
|
63 |
-} |